aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/activitypub/actor.ts4
-rw-r--r--server/lib/activitypub/process/process-create.ts17
-rw-r--r--server/lib/activitypub/process/process-like.ts5
-rw-r--r--server/lib/activitypub/process/process-update.ts3
-rw-r--r--server/lib/activitypub/videos.ts125
-rw-r--r--server/lib/emailer.ts7
-rw-r--r--server/lib/job-queue/handlers/activitypub-refresher.ts41
-rw-r--r--server/lib/job-queue/handlers/video-file.ts4
-rw-r--r--server/lib/job-queue/handlers/video-import.ts9
-rw-r--r--server/lib/job-queue/handlers/video-views.ts15
-rw-r--r--server/lib/job-queue/job-queue.ts8
-rw-r--r--server/lib/redis.ts9
-rw-r--r--server/lib/schedulers/videos-redundancy-scheduler.ts4
13 files changed, 160 insertions, 91 deletions
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index 504263c99..bbe48833d 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -178,9 +178,7 @@ async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
178 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] 178 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
179 179
180 const avatarName = uuidv4() + extension 180 const avatarName = uuidv4() + extension
181 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) 181 await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE)
182
183 await downloadImage(actorJSON.icon.url, destPath, AVATARS_SIZE)
184 182
185 return avatarName 183 return avatarName
186 } 184 }
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index 9a72cb899..df05ee452 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -12,9 +12,7 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
12import { forwardVideoRelatedActivity } from '../send/utils' 12import { forwardVideoRelatedActivity } from '../send/utils'
13import { Redis } from '../../redis' 13import { Redis } from '../../redis'
14import { createOrUpdateCacheFile } from '../cache-file' 14import { createOrUpdateCacheFile } from '../cache-file'
15import { immutableAssign } from '../../../../shared/utils'
16import { getVideoDislikeActivityPubUrl } from '../url' 15import { getVideoDislikeActivityPubUrl } from '../url'
17import { VideoModel } from '../../../models/video/video'
18 16
19async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { 17async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) {
20 const activityObject = activity.object 18 const activityObject = activity.object
@@ -71,7 +69,7 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
71 69
72 const [ , created ] = await AccountVideoRateModel.findOrCreate({ 70 const [ , created ] = await AccountVideoRateModel.findOrCreate({
73 where: rate, 71 where: rate,
74 defaults: immutableAssign(rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }), 72 defaults: Object.assign({}, rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }),
75 transaction: t 73 transaction: t
76 }) 74 })
77 if (created === true) await video.increment('dislikes', { transaction: t }) 75 if (created === true) await video.increment('dislikes', { transaction: t })
@@ -88,10 +86,19 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
88async function processCreateView (byActor: ActorModel, activity: ActivityCreate) { 86async function processCreateView (byActor: ActorModel, activity: ActivityCreate) {
89 const view = activity.object as ViewObject 87 const view = activity.object as ViewObject
90 88
91 const video = await VideoModel.loadByUrl(view.object) 89 const options = {
92 if (!video || video.isOwned() === false) return 90 videoObject: view.object,
91 fetchType: 'only-video' as 'only-video'
92 }
93 const { video } = await getOrCreateVideoAndAccountAndChannel(options)
93 94
94 await Redis.Instance.addVideoView(video.id) 95 await Redis.Instance.addVideoView(video.id)
96
97 if (video.isOwned()) {
98 // Don't resend the activity to the sender
99 const exceptions = [ byActor ]
100 await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
101 }
95} 102}
96 103
97async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) { 104async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) {
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts
index be86665e9..e8e97eece 100644
--- a/server/lib/activitypub/process/process-like.ts
+++ b/server/lib/activitypub/process/process-like.ts
@@ -5,8 +5,7 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
5import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
6import { forwardVideoRelatedActivity } from '../send/utils' 6import { forwardVideoRelatedActivity } from '../send/utils'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos' 7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { immutableAssign } from '../../../../shared/utils' 8import { getVideoLikeActivityPubUrl } from '../url'
9import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
10 9
11async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { 10async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) {
12 return retryTransactionWrapper(processLikeVideo, byActor, activity) 11 return retryTransactionWrapper(processLikeVideo, byActor, activity)
@@ -36,7 +35,7 @@ async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) {
36 } 35 }
37 const [ , created ] = await AccountVideoRateModel.findOrCreate({ 36 const [ , created ] = await AccountVideoRateModel.findOrCreate({
38 where: rate, 37 where: rate,
39 defaults: immutableAssign(rate, { url: getVideoLikeActivityPubUrl(byActor, video) }), 38 defaults: Object.assign({}, rate, { url: getVideoLikeActivityPubUrl(byActor, video) }),
40 transaction: t 39 transaction: t
41 }) 40 })
42 if (created === true) await video.increment('likes', { transaction: t }) 41 if (created === true) await video.increment('likes', { transaction: t })
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index bd4013555..c6b42d846 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -51,7 +51,7 @@ async function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate)
51 return undefined 51 return undefined
52 } 52 }
53 53
54 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id }) 54 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id, allowRefresh: false })
55 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) 55 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
56 56
57 const updateOptions = { 57 const updateOptions = {
@@ -59,7 +59,6 @@ async function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate)
59 videoObject, 59 videoObject,
60 account: actor.Account, 60 account: actor.Account,
61 channel: channelActor.VideoChannel, 61 channel: channelActor.VideoChannel,
62 updateViews: true,
63 overrideTo: activity.to 62 overrideTo: activity.to
64 } 63 }
65 return updateVideoFromAP(updateOptions) 64 return updateVideoFromAP(updateOptions)
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 4cecf9345..3d17e6846 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -95,9 +95,8 @@ function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Fu
95 95
96function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { 96function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) {
97 const thumbnailName = video.getThumbnailName() 97 const thumbnailName = video.getThumbnailName()
98 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
99 98
100 return downloadImage(icon.url, thumbnailPath, THUMBNAILS_SIZE) 99 return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE)
101} 100}
102 101
103function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { 102function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
@@ -117,7 +116,7 @@ type SyncParam = {
117 shares: boolean 116 shares: boolean
118 comments: boolean 117 comments: boolean
119 thumbnail: boolean 118 thumbnail: boolean
120 refreshVideo: boolean 119 refreshVideo?: boolean
121} 120}
122async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) { 121async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) {
123 logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid) 122 logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
@@ -159,26 +158,29 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
159 videoObject: VideoTorrentObject | string, 158 videoObject: VideoTorrentObject | string,
160 syncParam?: SyncParam, 159 syncParam?: SyncParam,
161 fetchType?: VideoFetchByUrlType, 160 fetchType?: VideoFetchByUrlType,
162 refreshViews?: boolean 161 allowRefresh?: boolean // true by default
163}) { 162}) {
164 // Default params 163 // Default params
165 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } 164 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
166 const fetchType = options.fetchType || 'all' 165 const fetchType = options.fetchType || 'all'
167 const refreshViews = options.refreshViews || false 166 const allowRefresh = options.allowRefresh !== false
168 167
169 // Get video url 168 // Get video url
170 const videoUrl = getAPUrl(options.videoObject) 169 const videoUrl = getAPUrl(options.videoObject)
171 170
172 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) 171 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
173 if (videoFromDatabase) { 172 if (videoFromDatabase) {
174 const refreshOptions = { 173
175 video: videoFromDatabase, 174 if (allowRefresh === true) {
176 fetchedType: fetchType, 175 const refreshOptions = {
177 syncParam, 176 video: videoFromDatabase,
178 refreshViews 177 fetchedType: fetchType,
178 syncParam
179 }
180
181 if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions)
182 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', videoUrl: videoFromDatabase.url } })
179 } 183 }
180 const p = refreshVideoIfNeeded(refreshOptions)
181 if (syncParam.refreshVideo === true) videoFromDatabase = await p
182 184
183 return { video: videoFromDatabase } 185 return { video: videoFromDatabase }
184 } 186 }
@@ -199,7 +201,6 @@ async function updateVideoFromAP (options: {
199 videoObject: VideoTorrentObject, 201 videoObject: VideoTorrentObject,
200 account: AccountModel, 202 account: AccountModel,
201 channel: VideoChannelModel, 203 channel: VideoChannelModel,
202 updateViews: boolean,
203 overrideTo?: string[] 204 overrideTo?: string[]
204}) { 205}) {
205 logger.debug('Updating remote video "%s".', options.videoObject.uuid) 206 logger.debug('Updating remote video "%s".', options.videoObject.uuid)
@@ -238,8 +239,8 @@ async function updateVideoFromAP (options: {
238 options.video.set('publishedAt', videoData.publishedAt) 239 options.video.set('publishedAt', videoData.publishedAt)
239 options.video.set('privacy', videoData.privacy) 240 options.video.set('privacy', videoData.privacy)
240 options.video.set('channelId', videoData.channelId) 241 options.video.set('channelId', videoData.channelId)
242 options.video.set('views', videoData.views)
241 243
242 if (options.updateViews === true) options.video.set('views', videoData.views)
243 await options.video.save(sequelizeOptions) 244 await options.video.save(sequelizeOptions)
244 245
245 { 246 {
@@ -297,8 +298,58 @@ async function updateVideoFromAP (options: {
297 } 298 }
298} 299}
299 300
301async function refreshVideoIfNeeded (options: {
302 video: VideoModel,
303 fetchedType: VideoFetchByUrlType,
304 syncParam: SyncParam
305}): Promise<VideoModel> {
306 if (!options.video.isOutdated()) return options.video
307
308 // We need more attributes if the argument video was fetched with not enough joints
309 const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
310
311 try {
312 const { response, videoObject } = await fetchRemoteVideo(video.url)
313 if (response.statusCode === 404) {
314 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
315
316 // Video does not exist anymore
317 await video.destroy()
318 return undefined
319 }
320
321 if (videoObject === undefined) {
322 logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
323
324 await video.setAsRefreshed()
325 return video
326 }
327
328 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
329 const account = await AccountModel.load(channelActor.VideoChannel.accountId)
330
331 const updateOptions = {
332 video,
333 videoObject,
334 account,
335 channel: channelActor.VideoChannel
336 }
337 await retryTransactionWrapper(updateVideoFromAP, updateOptions)
338 await syncVideoExternalAttributes(video, videoObject, options.syncParam)
339
340 return video
341 } catch (err) {
342 logger.warn('Cannot refresh video %s.', options.video.url, { err })
343
344 // Don't refresh in loop
345 await video.setAsRefreshed()
346 return video
347 }
348}
349
300export { 350export {
301 updateVideoFromAP, 351 updateVideoFromAP,
352 refreshVideoIfNeeded,
302 federateVideoIfNeeded, 353 federateVideoIfNeeded,
303 fetchRemoteVideo, 354 fetchRemoteVideo,
304 getOrCreateVideoAndAccountAndChannel, 355 getOrCreateVideoAndAccountAndChannel,
@@ -362,52 +413,6 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
362 return videoCreated 413 return videoCreated
363} 414}
364 415
365async function refreshVideoIfNeeded (options: {
366 video: VideoModel,
367 fetchedType: VideoFetchByUrlType,
368 syncParam: SyncParam,
369 refreshViews: boolean
370}): Promise<VideoModel> {
371 if (!options.video.isOutdated()) return options.video
372
373 // We need more attributes if the argument video was fetched with not enough joints
374 const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
375
376 try {
377 const { response, videoObject } = await fetchRemoteVideo(video.url)
378 if (response.statusCode === 404) {
379 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
380
381 // Video does not exist anymore
382 await video.destroy()
383 return undefined
384 }
385
386 if (videoObject === undefined) {
387 logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
388 return video
389 }
390
391 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
392 const account = await AccountModel.load(channelActor.VideoChannel.accountId)
393
394 const updateOptions = {
395 video,
396 videoObject,
397 account,
398 channel: channelActor.VideoChannel,
399 updateViews: options.refreshViews
400 }
401 await retryTransactionWrapper(updateVideoFromAP, updateOptions)
402 await syncVideoExternalAttributes(video, videoObject, options.syncParam)
403
404 return video
405 } catch (err) {
406 logger.warn('Cannot refresh video %s.', options.video.url, { err })
407 return video
408 }
409}
410
411async function videoActivityObjectToDBAttributes ( 416async function videoActivityObjectToDBAttributes (
412 videoChannel: VideoChannelModel, 417 videoChannel: VideoChannelModel,
413 videoObject: VideoTorrentObject, 418 videoObject: VideoTorrentObject,
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 9327792fb..074d4ad44 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -14,6 +14,7 @@ class Emailer {
14 private static instance: Emailer 14 private static instance: Emailer
15 private initialized = false 15 private initialized = false
16 private transporter: Transporter 16 private transporter: Transporter
17 private enabled = false
17 18
18 private constructor () {} 19 private constructor () {}
19 20
@@ -50,6 +51,8 @@ class Emailer {
50 tls, 51 tls,
51 auth 52 auth
52 }) 53 })
54
55 this.enabled = true
53 } else { 56 } else {
54 if (!isTestInstance()) { 57 if (!isTestInstance()) {
55 logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!') 58 logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!')
@@ -57,6 +60,10 @@ class Emailer {
57 } 60 }
58 } 61 }
59 62
63 isEnabled () {
64 return this.enabled
65 }
66
60 async checkConnectionOrDie () { 67 async checkConnectionOrDie () {
61 if (!this.transporter) return 68 if (!this.transporter) return
62 69
diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts
new file mode 100644
index 000000000..671b0f487
--- /dev/null
+++ b/server/lib/job-queue/handlers/activitypub-refresher.ts
@@ -0,0 +1,41 @@
1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger'
3import { fetchVideoByUrl } from '../../../helpers/video'
4import { refreshVideoIfNeeded } from '../../activitypub'
5
6export type RefreshPayload = {
7 videoUrl: string
8 type: 'video'
9}
10
11async function refreshAPObject (job: Bull.Job) {
12 const payload = job.data as RefreshPayload
13
14 logger.info('Processing AP refresher in job %d for video %s.', job.id, payload.videoUrl)
15
16 if (payload.type === 'video') return refreshAPVideo(payload.videoUrl)
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 refreshAPObject
23}
24
25// ---------------------------------------------------------------------------
26
27async function refreshAPVideo (videoUrl: string) {
28 const fetchType = 'all' as 'all'
29 const syncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true }
30
31 const videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
32 if (videoFromDatabase) {
33 const refreshOptions = {
34 video: videoFromDatabase,
35 fetchedType: fetchType,
36 syncParam
37 }
38
39 await refreshVideoIfNeeded(refreshOptions)
40 }
41}
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts
index adc0a2a15..ddbf6d1c2 100644
--- a/server/lib/job-queue/handlers/video-file.ts
+++ b/server/lib/job-queue/handlers/video-file.ts
@@ -1,5 +1,5 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { VideoResolution, VideoState } from '../../../../shared' 2import { VideoResolution, VideoState, Job } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { VideoModel } from '../../../models/video/video' 4import { VideoModel } from '../../../models/video/video'
5import { JobQueue } from '../job-queue' 5import { JobQueue } from '../job-queue'
@@ -111,7 +111,7 @@ async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boole
111 ) 111 )
112 112
113 if (resolutionsEnabled.length !== 0) { 113 if (resolutionsEnabled.length !== 0) {
114 const tasks: Bluebird<any>[] = [] 114 const tasks: Bluebird<Bull.Job<any>>[] = []
115 115
116 for (const resolution of resolutionsEnabled) { 116 for (const resolution of resolutionsEnabled) {
117 const dataInput = { 117 const dataInput = {
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 4de901c0c..51a0b5faf 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -7,7 +7,7 @@ import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } fro
7import { extname, join } from 'path' 7import { extname, join } from 'path'
8import { VideoFileModel } from '../../../models/video/video-file' 8import { VideoFileModel } from '../../../models/video/video-file'
9import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' 9import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers'
10import { doRequestAndSaveToFile, downloadImage } from '../../../helpers/requests' 10import { downloadImage } from '../../../helpers/requests'
11import { VideoState } from '../../../../shared' 11import { VideoState } from '../../../../shared'
12import { JobQueue } from '../index' 12import { JobQueue } from '../index'
13import { federateVideoIfNeeded } from '../../activitypub' 13import { federateVideoIfNeeded } from '../../activitypub'
@@ -109,6 +109,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
109 let tempVideoPath: string 109 let tempVideoPath: string
110 let videoDestFile: string 110 let videoDestFile: string
111 let videoFile: VideoFileModel 111 let videoFile: VideoFileModel
112
112 try { 113 try {
113 // Download video from youtubeDL 114 // Download video from youtubeDL
114 tempVideoPath = await downloader() 115 tempVideoPath = await downloader()
@@ -144,8 +145,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
144 // Process thumbnail 145 // Process thumbnail
145 if (options.downloadThumbnail) { 146 if (options.downloadThumbnail) {
146 if (options.thumbnailUrl) { 147 if (options.thumbnailUrl) {
147 const destThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName()) 148 await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName(), THUMBNAILS_SIZE)
148 await downloadImage(options.thumbnailUrl, destThumbnailPath, THUMBNAILS_SIZE)
149 } else { 149 } else {
150 await videoImport.Video.createThumbnail(videoFile) 150 await videoImport.Video.createThumbnail(videoFile)
151 } 151 }
@@ -156,8 +156,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
156 // Process preview 156 // Process preview
157 if (options.downloadPreview) { 157 if (options.downloadPreview) {
158 if (options.thumbnailUrl) { 158 if (options.thumbnailUrl) {
159 const destPreviewPath = join(CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName()) 159 await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName(), PREVIEWS_SIZE)
160 await downloadImage(options.thumbnailUrl, destPreviewPath, PREVIEWS_SIZE)
161 } else { 160 } else {
162 await videoImport.Video.createPreview(videoFile) 161 await videoImport.Video.createPreview(videoFile)
163 } 162 }
diff --git a/server/lib/job-queue/handlers/video-views.ts b/server/lib/job-queue/handlers/video-views.ts
index f44c3c727..fa1fd13b3 100644
--- a/server/lib/job-queue/handlers/video-views.ts
+++ b/server/lib/job-queue/handlers/video-views.ts
@@ -23,13 +23,9 @@ async function processVideosViews () {
23 for (const videoId of videoIds) { 23 for (const videoId of videoIds) {
24 try { 24 try {
25 const views = await Redis.Instance.getVideoViews(videoId, hour) 25 const views = await Redis.Instance.getVideoViews(videoId, hour)
26 if (isNaN(views)) { 26 if (views) {
27 logger.error('Cannot process videos views of video %d in hour %d: views number is NaN.', videoId, hour)
28 } else {
29 logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour) 27 logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour)
30 28
31 await VideoModel.incrementViews(videoId, views)
32
33 try { 29 try {
34 await VideoViewModel.create({ 30 await VideoViewModel.create({
35 startDate, 31 startDate,
@@ -39,7 +35,14 @@ async function processVideosViews () {
39 }) 35 })
40 36
41 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) 37 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
42 if (video.isOwned()) await federateVideoIfNeeded(video, false) 38 if (video.isOwned()) {
39 // If this is a remote video, the origin instance will send us an update
40 await VideoModel.incrementViews(videoId, views)
41
42 // Send video update
43 video.views += views
44 await federateVideoIfNeeded(video, false)
45 }
43 } catch (err) { 46 } catch (err) {
44 logger.debug('Cannot create video views for video %d in hour %d. Maybe the video does not exist anymore?', videoId, hour) 47 logger.debug('Cannot create video views for video %d in hour %d. Maybe the video does not exist anymore?', videoId, hour)
45 } 48 }
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts
index 4cfd4d253..5862e178f 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -11,6 +11,7 @@ import { processVideoFile, processVideoFileImport, VideoFileImportPayload, Video
11import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow' 11import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow'
12import { processVideoImport, VideoImportPayload } from './handlers/video-import' 12import { processVideoImport, VideoImportPayload } from './handlers/video-import'
13import { processVideosViews } from './handlers/video-views' 13import { processVideosViews } from './handlers/video-views'
14import { refreshAPObject, RefreshPayload } from './handlers/activitypub-refresher'
14 15
15type CreateJobArgument = 16type CreateJobArgument =
16 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | 17 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
@@ -21,6 +22,7 @@ type CreateJobArgument =
21 { type: 'video-file', payload: VideoFilePayload } | 22 { type: 'video-file', payload: VideoFilePayload } |
22 { type: 'email', payload: EmailPayload } | 23 { type: 'email', payload: EmailPayload } |
23 { type: 'video-import', payload: VideoImportPayload } | 24 { type: 'video-import', payload: VideoImportPayload } |
25 { type: 'activitypub-refresher', payload: RefreshPayload } |
24 { type: 'videos-views', payload: {} } 26 { type: 'videos-views', payload: {} }
25 27
26const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = { 28const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = {
@@ -32,7 +34,8 @@ const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = {
32 'video-file': processVideoFile, 34 'video-file': processVideoFile,
33 'email': processEmail, 35 'email': processEmail,
34 'video-import': processVideoImport, 36 'video-import': processVideoImport,
35 'videos-views': processVideosViews 37 'videos-views': processVideosViews,
38 'activitypub-refresher': refreshAPObject
36} 39}
37 40
38const jobTypes: JobType[] = [ 41const jobTypes: JobType[] = [
@@ -44,7 +47,8 @@ const jobTypes: JobType[] = [
44 'video-file', 47 'video-file',
45 'video-file-import', 48 'video-file-import',
46 'video-import', 49 'video-import',
47 'videos-views' 50 'videos-views',
51 'activitypub-refresher'
48] 52]
49 53
50class JobQueue { 54class JobQueue {
diff --git a/server/lib/redis.ts b/server/lib/redis.ts
index abd75d512..3e25e6a2c 100644
--- a/server/lib/redis.ts
+++ b/server/lib/redis.ts
@@ -121,7 +121,14 @@ class Redis {
121 const key = this.generateVideoViewKey(videoId, hour) 121 const key = this.generateVideoViewKey(videoId, hour)
122 122
123 const valueString = await this.getValue(key) 123 const valueString = await this.getValue(key)
124 return parseInt(valueString, 10) 124 const valueInt = parseInt(valueString, 10)
125
126 if (isNaN(valueInt)) {
127 logger.error('Cannot get videos views of video %d in hour %d: views number is NaN (%s).', videoId, hour, valueString)
128 return undefined
129 }
130
131 return valueInt
125 } 132 }
126 133
127 async getVideosIdViewed (hour: number) { 134 async getVideosIdViewed (hour: number) {
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts
index 8b7f33539..2a99a665d 100644
--- a/server/lib/schedulers/videos-redundancy-scheduler.ts
+++ b/server/lib/schedulers/videos-redundancy-scheduler.ts
@@ -145,13 +145,13 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
145 145
146 const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) 146 const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT)
147 147
148 const destPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(file)) 148 const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file))
149 await rename(tmpPath, destPath) 149 await rename(tmpPath, destPath)
150 150
151 const createdModel = await VideoRedundancyModel.create({ 151 const createdModel = await VideoRedundancyModel.create({
152 expiresOn: this.buildNewExpiration(redundancy.minLifetime), 152 expiresOn: this.buildNewExpiration(redundancy.minLifetime),
153 url: getVideoCacheFileActivityPubUrl(file), 153 url: getVideoCacheFileActivityPubUrl(file),
154 fileUrl: video.getVideoFileUrl(file, CONFIG.WEBSERVER.URL), 154 fileUrl: video.getVideoRedundancyUrl(file, CONFIG.WEBSERVER.URL),
155 strategy: redundancy.strategy, 155 strategy: redundancy.strategy,
156 videoFileId: file.id, 156 videoFileId: file.id,
157 actorId: serverActor.id 157 actorId: serverActor.id