aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-04-17 10:07:00 +0200
committerChocobozzz <me@florianbigard.com>2019-04-24 16:25:52 +0200
commite8bafea35bc930cb8ac5b2d521a188642a1adffe (patch)
tree7537f957ed7307b464e3c90b71b813d992acaade /server/lib/activitypub
parent94565d52bb2883e09f16d1363170ac9c0dccb7a1 (diff)
downloadPeerTube-e8bafea35bc930cb8ac5b2d521a188642a1adffe.tar.gz
PeerTube-e8bafea35bc930cb8ac5b2d521a188642a1adffe.tar.zst
PeerTube-e8bafea35bc930cb8ac5b2d521a188642a1adffe.zip
Create a dedicated table to track video thumbnails
Diffstat (limited to 'server/lib/activitypub')
-rw-r--r--server/lib/activitypub/playlist.ts25
-rw-r--r--server/lib/activitypub/videos.ts92
2 files changed, 79 insertions, 38 deletions
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts
index f312409bc..341e469f3 100644
--- a/server/lib/activitypub/playlist.ts
+++ b/server/lib/activitypub/playlist.ts
@@ -1,12 +1,12 @@
1import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' 1import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
2import { crawlCollectionPage } from './crawl' 2import { crawlCollectionPage } from './crawl'
3import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY, THUMBNAILS_SIZE } from '../../initializers/constants' 3import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
4import { AccountModel } from '../../models/account/account' 4import { AccountModel } from '../../models/account/account'
5import { isArray } from '../../helpers/custom-validators/misc' 5import { isArray } from '../../helpers/custom-validators/misc'
6import { getOrCreateActorAndServerAndModel } from './actor' 6import { getOrCreateActorAndServerAndModel } from './actor'
7import { logger } from '../../helpers/logger' 7import { logger } from '../../helpers/logger'
8import { VideoPlaylistModel } from '../../models/video/video-playlist' 8import { VideoPlaylistModel } from '../../models/video/video-playlist'
9import { doRequest, downloadImage } from '../../helpers/requests' 9import { doRequest } from '../../helpers/requests'
10import { checkUrlsSameHost } from '../../helpers/activitypub' 10import { checkUrlsSameHost } from '../../helpers/activitypub'
11import * as Bluebird from 'bluebird' 11import * as Bluebird from 'bluebird'
12import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' 12import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
@@ -16,9 +16,8 @@ import { VideoPlaylistElementModel } from '../../models/video/video-playlist-ele
16import { VideoModel } from '../../models/video/video' 16import { VideoModel } from '../../models/video/video'
17import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' 17import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
18import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' 18import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
19import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
20import { CONFIG } from '../../initializers/config'
21import { sequelizeTypescript } from '../../initializers/database' 19import { sequelizeTypescript } from '../../initializers/database'
20import { createPlaylistThumbnailFromUrl } from '../thumbnail'
22 21
23function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) { 22function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) {
24 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED 23 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED
@@ -97,16 +96,20 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
97 return Promise.resolve() 96 return Promise.resolve()
98 }) 97 })
99 98
100 // Empty playlists generally do not have a miniature, so skip this 99 const refreshedPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(playlist.id, null)
101 if (accItems.length !== 0) { 100
101 if (playlistObject.icon) {
102 try { 102 try {
103 await generateThumbnailFromUrl(playlist, playlistObject.icon) 103 const thumbnailModel = await createPlaylistThumbnailFromUrl(playlistObject.icon.url, refreshedPlaylist)
104 thumbnailModel.videoPlaylistId = refreshedPlaylist.id
105
106 refreshedPlaylist.setThumbnail(await thumbnailModel.save())
104 } catch (err) { 107 } catch (err) {
105 logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err }) 108 logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err })
106 } 109 }
107 } 110 }
108 111
109 return resetVideoPlaylistElements(accItems, playlist) 112 return resetVideoPlaylistElements(accItems, refreshedPlaylist)
110} 113}
111 114
112async function refreshVideoPlaylistIfNeeded (videoPlaylist: VideoPlaylistModel): Promise<VideoPlaylistModel> { 115async function refreshVideoPlaylistIfNeeded (videoPlaylist: VideoPlaylistModel): Promise<VideoPlaylistModel> {
@@ -191,12 +194,6 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: Vide
191 return undefined 194 return undefined
192} 195}
193 196
194function generateThumbnailFromUrl (playlist: VideoPlaylistModel, icon: ActivityIconObject) {
195 const thumbnailName = playlist.getThumbnailName()
196
197 return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE)
198}
199
200async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { 197async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> {
201 const options = { 198 const options = {
202 uri: playlistUrl, 199 uri: playlistUrl,
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index b9252e363..16c37a55f 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -3,11 +3,10 @@ import * as sequelize from 'sequelize'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import * as request from 'request' 4import * as request from 'request'
5import { 5import {
6 ActivityIconObject,
7 ActivityPlaylistSegmentHashesObject, 6 ActivityPlaylistSegmentHashesObject,
8 ActivityPlaylistUrlObject, 7 ActivityPlaylistUrlObject,
9 ActivityUrlObject, 8 ActivityUrlObject,
10 ActivityVideoUrlObject, 9 ActivityVideoUrlObject, VideoCreate,
11 VideoState 10 VideoState
12} from '../../../shared/index' 11} from '../../../shared/index'
13import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 12import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
@@ -16,8 +15,15 @@ import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validat
16import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 15import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
17import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 16import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
18import { logger } from '../../helpers/logger' 17import { logger } from '../../helpers/logger'
19import { doRequest, downloadImage } from '../../helpers/requests' 18import { doRequest } from '../../helpers/requests'
20import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, REMOTE_SCHEME, THUMBNAILS_SIZE } from '../../initializers/constants' 19import {
20 ACTIVITY_PUB,
21 MIMETYPES,
22 P2P_MEDIA_LOADER_PEER_VERSION,
23 PREVIEWS_SIZE,
24 REMOTE_SCHEME,
25 STATIC_PATHS
26} from '../../initializers/constants'
21import { ActorModel } from '../../models/activitypub/actor' 27import { ActorModel } from '../../models/activitypub/actor'
22import { TagModel } from '../../models/video/tag' 28import { TagModel } from '../../models/video/tag'
23import { VideoModel } from '../../models/video/video' 29import { VideoModel } from '../../models/video/video'
@@ -43,8 +49,11 @@ import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
43import { AccountVideoRateModel } from '../../models/account/account-video-rate' 49import { AccountVideoRateModel } from '../../models/account/account-video-rate'
44import { VideoShareModel } from '../../models/video/video-share' 50import { VideoShareModel } from '../../models/video/video-share'
45import { VideoCommentModel } from '../../models/video/video-comment' 51import { VideoCommentModel } from '../../models/video/video-comment'
46import { CONFIG } from '../../initializers/config'
47import { sequelizeTypescript } from '../../initializers/database' 52import { sequelizeTypescript } from '../../initializers/database'
53import { createPlaceholderThumbnail, createVideoThumbnailFromUrl } from '../thumbnail'
54import { ThumbnailModel } from '../../models/video/thumbnail'
55import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
56import { join } from 'path'
48 57
49async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 58async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
50 // If the video is not private and is published, we federate it 59 // If the video is not private and is published, we federate it
@@ -100,18 +109,18 @@ async function fetchRemoteVideoDescription (video: VideoModel) {
100} 109}
101 110
102function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Function) { 111function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Function) {
103 const host = video.VideoChannel.Account.Actor.Server.host 112 const url = buildRemoteBaseUrl(video, path)
104 113
105 // We need to provide a callback, if no we could have an uncaught exception 114 // We need to provide a callback, if no we could have an uncaught exception
106 return request.get(REMOTE_SCHEME.HTTP + '://' + host + path, err => { 115 return request.get(url, err => {
107 if (err) reject(err) 116 if (err) reject(err)
108 }) 117 })
109} 118}
110 119
111function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { 120function buildRemoteBaseUrl (video: VideoModel, path: string) {
112 const thumbnailName = video.getThumbnailName() 121 const host = video.VideoChannel.Account.Actor.Server.host
113 122
114 return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE) 123 return REMOTE_SCHEME.HTTP + '://' + host + path
115} 124}
116 125
117function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { 126function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
@@ -236,6 +245,14 @@ async function updateVideoFromAP (options: {
236 const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED 245 const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED
237 246
238 try { 247 try {
248 let thumbnailModel: ThumbnailModel
249
250 try {
251 thumbnailModel = await createVideoThumbnailFromUrl(options.videoObject.icon.url, options.video, ThumbnailType.THUMBNAIL)
252 } catch (err) {
253 logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err })
254 }
255
239 await sequelizeTypescript.transaction(async t => { 256 await sequelizeTypescript.transaction(async t => {
240 const sequelizeOptions = { transaction: t } 257 const sequelizeOptions = { transaction: t }
241 258
@@ -272,6 +289,17 @@ async function updateVideoFromAP (options: {
272 289
273 await options.video.save(sequelizeOptions) 290 await options.video.save(sequelizeOptions)
274 291
292 if (thumbnailModel) {
293 thumbnailModel.videoId = options.video.id
294 options.video.addThumbnail(await thumbnailModel.save({ transaction: t }))
295 }
296
297 // FIXME: use icon URL instead
298 const previewUrl = buildRemoteBaseUrl(options.video, join(STATIC_PATHS.PREVIEWS, options.video.getPreview().filename))
299 const previewModel = createPlaceholderThumbnail(previewUrl, options.video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
300
301 options.video.addThumbnail(await previewModel.save({ transaction: t }))
302
275 { 303 {
276 const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject) 304 const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject)
277 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) 305 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
@@ -347,12 +375,6 @@ async function updateVideoFromAP (options: {
347 logger.debug('Cannot update the remote video.', { err }) 375 logger.debug('Cannot update the remote video.', { err })
348 throw err 376 throw err
349 } 377 }
350
351 try {
352 await generateThumbnailFromUrl(options.video, options.videoObject.icon)
353 } catch (err) {
354 logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err })
355 }
356} 378}
357 379
358async function refreshVideoIfNeeded (options: { 380async function refreshVideoIfNeeded (options: {
@@ -412,7 +434,6 @@ export {
412 getOrCreateVideoAndAccountAndChannel, 434 getOrCreateVideoAndAccountAndChannel,
413 fetchRemoteVideoStaticFile, 435 fetchRemoteVideoStaticFile,
414 fetchRemoteVideoDescription, 436 fetchRemoteVideoDescription,
415 generateThumbnailFromUrl,
416 getOrCreateVideoChannelFromVideoObject 437 getOrCreateVideoChannelFromVideoObject
417} 438}
418 439
@@ -440,13 +461,34 @@ function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistS
440async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { 461async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
441 logger.debug('Adding remote video %s.', videoObject.id) 462 logger.debug('Adding remote video %s.', videoObject.id)
442 463
464 const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
465 const video = VideoModel.build(videoData)
466
467 const promiseThumbnail = createVideoThumbnailFromUrl(videoObject.icon.url, video, ThumbnailType.THUMBNAIL)
468
469 let thumbnailModel: ThumbnailModel
470 if (waitThumbnail === true) {
471 thumbnailModel = await promiseThumbnail
472 }
473
443 const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => { 474 const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => {
444 const sequelizeOptions = { transaction: t } 475 const sequelizeOptions = { transaction: t }
445 476
446 const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
447 const video = VideoModel.build(videoData)
448
449 const videoCreated = await video.save(sequelizeOptions) 477 const videoCreated = await video.save(sequelizeOptions)
478 videoCreated.VideoChannel = channelActor.VideoChannel
479
480 if (thumbnailModel) {
481 thumbnailModel.videoId = videoCreated.id
482
483 videoCreated.addThumbnail(await thumbnailModel.save({ transaction: t }))
484 }
485
486 // FIXME: use icon URL instead
487 const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
488 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
489 previewModel.videoId = videoCreated.id
490
491 videoCreated.addThumbnail(await previewModel.save({ transaction: t }))
450 492
451 // Process files 493 // Process files
452 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject) 494 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject)
@@ -476,14 +518,16 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
476 518
477 logger.info('Remote video with uuid %s inserted.', videoObject.uuid) 519 logger.info('Remote video with uuid %s inserted.', videoObject.uuid)
478 520
479 videoCreated.VideoChannel = channelActor.VideoChannel
480 return videoCreated 521 return videoCreated
481 }) 522 })
482 523
483 const p = generateThumbnailFromUrl(videoCreated, videoObject.icon) 524 if (waitThumbnail === false) {
484 .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err })) 525 promiseThumbnail.then(thumbnailModel => {
526 thumbnailModel = videoCreated.id
485 527
486 if (waitThumbnail === true) await p 528 return thumbnailModel.save()
529 })
530 }
487 531
488 return videoCreated 532 return videoCreated
489} 533}