diff options
author | Chocobozzz <me@florianbigard.com> | 2019-04-17 10:07:00 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-04-24 16:25:52 +0200 |
commit | e8bafea35bc930cb8ac5b2d521a188642a1adffe (patch) | |
tree | 7537f957ed7307b464e3c90b71b813d992acaade /server/lib/activitypub | |
parent | 94565d52bb2883e09f16d1363170ac9c0dccb7a1 (diff) | |
download | PeerTube-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.ts | 25 | ||||
-rw-r--r-- | server/lib/activitypub/videos.ts | 92 |
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 @@ | |||
1 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | 1 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' |
2 | import { crawlCollectionPage } from './crawl' | 2 | import { crawlCollectionPage } from './crawl' |
3 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY, THUMBNAILS_SIZE } from '../../initializers/constants' | 3 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' |
4 | import { AccountModel } from '../../models/account/account' | 4 | import { AccountModel } from '../../models/account/account' |
5 | import { isArray } from '../../helpers/custom-validators/misc' | 5 | import { isArray } from '../../helpers/custom-validators/misc' |
6 | import { getOrCreateActorAndServerAndModel } from './actor' | 6 | import { getOrCreateActorAndServerAndModel } from './actor' |
7 | import { logger } from '../../helpers/logger' | 7 | import { logger } from '../../helpers/logger' |
8 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 8 | import { VideoPlaylistModel } from '../../models/video/video-playlist' |
9 | import { doRequest, downloadImage } from '../../helpers/requests' | 9 | import { doRequest } from '../../helpers/requests' |
10 | import { checkUrlsSameHost } from '../../helpers/activitypub' | 10 | import { checkUrlsSameHost } from '../../helpers/activitypub' |
11 | import * as Bluebird from 'bluebird' | 11 | import * as Bluebird from 'bluebird' |
12 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | 12 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' |
@@ -16,9 +16,8 @@ import { VideoPlaylistElementModel } from '../../models/video/video-playlist-ele | |||
16 | import { VideoModel } from '../../models/video/video' | 16 | import { VideoModel } from '../../models/video/video' |
17 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' | 17 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' |
18 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | 18 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' |
19 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | ||
20 | import { CONFIG } from '../../initializers/config' | ||
21 | import { sequelizeTypescript } from '../../initializers/database' | 19 | import { sequelizeTypescript } from '../../initializers/database' |
20 | import { createPlaylistThumbnailFromUrl } from '../thumbnail' | ||
22 | 21 | ||
23 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) { | 22 | function 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 | ||
112 | async function refreshVideoPlaylistIfNeeded (videoPlaylist: VideoPlaylistModel): Promise<VideoPlaylistModel> { | 115 | async 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 | ||
194 | function 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 | |||
200 | async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { | 197 | async 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' | |||
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import * as request from 'request' | 4 | import * as request from 'request' |
5 | import { | 5 | import { |
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' |
13 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 12 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
@@ -16,8 +15,15 @@ import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validat | |||
16 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 15 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
17 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' | 16 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' |
18 | import { logger } from '../../helpers/logger' | 17 | import { logger } from '../../helpers/logger' |
19 | import { doRequest, downloadImage } from '../../helpers/requests' | 18 | import { doRequest } from '../../helpers/requests' |
20 | import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, REMOTE_SCHEME, THUMBNAILS_SIZE } from '../../initializers/constants' | 19 | import { |
20 | ACTIVITY_PUB, | ||
21 | MIMETYPES, | ||
22 | P2P_MEDIA_LOADER_PEER_VERSION, | ||
23 | PREVIEWS_SIZE, | ||
24 | REMOTE_SCHEME, | ||
25 | STATIC_PATHS | ||
26 | } from '../../initializers/constants' | ||
21 | import { ActorModel } from '../../models/activitypub/actor' | 27 | import { ActorModel } from '../../models/activitypub/actor' |
22 | import { TagModel } from '../../models/video/tag' | 28 | import { TagModel } from '../../models/video/tag' |
23 | import { VideoModel } from '../../models/video/video' | 29 | import { VideoModel } from '../../models/video/video' |
@@ -43,8 +49,11 @@ import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' | |||
43 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | 49 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
44 | import { VideoShareModel } from '../../models/video/video-share' | 50 | import { VideoShareModel } from '../../models/video/video-share' |
45 | import { VideoCommentModel } from '../../models/video/video-comment' | 51 | import { VideoCommentModel } from '../../models/video/video-comment' |
46 | import { CONFIG } from '../../initializers/config' | ||
47 | import { sequelizeTypescript } from '../../initializers/database' | 52 | import { sequelizeTypescript } from '../../initializers/database' |
53 | import { createPlaceholderThumbnail, createVideoThumbnailFromUrl } from '../thumbnail' | ||
54 | import { ThumbnailModel } from '../../models/video/thumbnail' | ||
55 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | ||
56 | import { join } from 'path' | ||
48 | 57 | ||
49 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { | 58 | async 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 | ||
102 | function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Function) { | 111 | function 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 | ||
111 | function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { | 120 | function 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 | ||
117 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { | 126 | function 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 | ||
358 | async function refreshVideoIfNeeded (options: { | 380 | async 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 | |||
440 | async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { | 461 | async 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 | } |