diff options
Diffstat (limited to 'server')
235 files changed, 5468 insertions, 2220 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 750e3091c..c47c61f52 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts | |||
@@ -4,6 +4,7 @@ import { activityPubCollectionPagination } from '@server/lib/activitypub/collect | |||
4 | import { activityPubContextify } from '@server/lib/activitypub/context' | 4 | import { activityPubContextify } from '@server/lib/activitypub/context' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
6 | import { MAccountId, MActorId, MChannelId, MVideoId } from '@server/types/models' | 6 | import { MAccountId, MActorId, MChannelId, MVideoId } from '@server/types/models' |
7 | import { VideoCommentObject } from '@shared/models' | ||
7 | import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' | 8 | import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' |
8 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | 9 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' |
9 | import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants' | 10 | import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants' |
@@ -33,7 +34,6 @@ import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from ' | |||
33 | import { AccountModel } from '../../models/account/account' | 34 | import { AccountModel } from '../../models/account/account' |
34 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | 35 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
35 | import { ActorFollowModel } from '../../models/actor/actor-follow' | 36 | import { ActorFollowModel } from '../../models/actor/actor-follow' |
36 | import { VideoCaptionModel } from '../../models/video/video-caption' | ||
37 | import { VideoCommentModel } from '../../models/video/video-comment' | 37 | import { VideoCommentModel } from '../../models/video/video-comment' |
38 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 38 | import { VideoPlaylistModel } from '../../models/video/video-playlist' |
39 | import { VideoShareModel } from '../../models/video/video-share' | 39 | import { VideoShareModel } from '../../models/video/video-share' |
@@ -242,14 +242,13 @@ async function videoController (req: express.Request, res: express.Response) { | |||
242 | if (redirectIfNotOwned(video.url, res)) return | 242 | if (redirectIfNotOwned(video.url, res)) return |
243 | 243 | ||
244 | // We need captions to render AP object | 244 | // We need captions to render AP object |
245 | const captions = await VideoCaptionModel.listVideoCaptions(video.id) | 245 | const videoAP = await video.lightAPToFullAP(undefined) |
246 | const videoWithCaptions = Object.assign(video, { VideoCaptions: captions }) | ||
247 | 246 | ||
248 | const audience = getAudience(videoWithCaptions.VideoChannel.Account.Actor, videoWithCaptions.privacy === VideoPrivacy.PUBLIC) | 247 | const audience = getAudience(videoAP.VideoChannel.Account.Actor, videoAP.privacy === VideoPrivacy.PUBLIC) |
249 | const videoObject = audiencify(await videoWithCaptions.toActivityPubObject(), audience) | 248 | const videoObject = audiencify(await videoAP.toActivityPubObject(), audience) |
250 | 249 | ||
251 | if (req.path.endsWith('/activity')) { | 250 | if (req.path.endsWith('/activity')) { |
252 | const data = buildCreateActivity(videoWithCaptions.url, video.VideoChannel.Account.Actor, videoObject, audience) | 251 | const data = buildCreateActivity(videoAP.url, video.VideoChannel.Account.Actor, videoObject, audience) |
253 | return activityPubResponse(activityPubContextify(data, 'Video'), res) | 252 | return activityPubResponse(activityPubContextify(data, 'Video'), res) |
254 | } | 253 | } |
255 | 254 | ||
@@ -355,7 +354,7 @@ async function videoCommentController (req: express.Request, res: express.Respon | |||
355 | videoCommentObject = audiencify(videoCommentObject, audience) | 354 | videoCommentObject = audiencify(videoCommentObject, audience) |
356 | 355 | ||
357 | if (req.path.endsWith('/activity')) { | 356 | if (req.path.endsWith('/activity')) { |
358 | const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject, audience) | 357 | const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject as VideoCommentObject, audience) |
359 | return activityPubResponse(activityPubContextify(data, 'Comment'), res) | 358 | return activityPubResponse(activityPubContextify(data, 'Comment'), res) |
360 | } | 359 | } |
361 | } | 360 | } |
diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts index 681a5660c..4175cf276 100644 --- a/server/controllers/activitypub/outbox.ts +++ b/server/controllers/activitypub/outbox.ts | |||
@@ -63,6 +63,7 @@ async function buildActivities (actor: MActorLight, start: number, count: number | |||
63 | 63 | ||
64 | activities.push(announceActivity) | 64 | activities.push(announceActivity) |
65 | } else { | 65 | } else { |
66 | // FIXME: only use the video URL to reduce load. Breaks compat with PeerTube < 6.0.0 | ||
66 | const videoObject = await video.toActivityPubObject() | 67 | const videoObject = await video.toActivityPubObject() |
67 | const createActivity = buildCreateActivity(video.url, byActor, videoObject, createActivityAudience) | 68 | const createActivity = buildCreateActivity(video.url, byActor, videoObject, createActivityAudience) |
68 | 69 | ||
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index 96f36bf6f..49cd7559a 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts | |||
@@ -2,7 +2,6 @@ import express from 'express' | |||
2 | import { pickCommonVideoQuery } from '@server/helpers/query' | 2 | import { pickCommonVideoQuery } from '@server/helpers/query' |
3 | import { ActorFollowModel } from '@server/models/actor/actor-follow' | 3 | import { ActorFollowModel } from '@server/models/actor/actor-follow' |
4 | import { getServerActor } from '@server/models/application/application' | 4 | import { getServerActor } from '@server/models/application/application' |
5 | import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' | ||
6 | import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' | 5 | import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' |
7 | import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | 6 | import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' |
8 | import { getFormattedObjects } from '../../helpers/utils' | 7 | import { getFormattedObjects } from '../../helpers/utils' |
@@ -36,6 +35,7 @@ import { | |||
36 | import { commonVideoPlaylistFiltersValidator, videoPlaylistsSearchValidator } from '../../middlewares/validators/videos/video-playlists' | 35 | import { commonVideoPlaylistFiltersValidator, videoPlaylistsSearchValidator } from '../../middlewares/validators/videos/video-playlists' |
37 | import { AccountModel } from '../../models/account/account' | 36 | import { AccountModel } from '../../models/account/account' |
38 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | 37 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
38 | import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter' | ||
39 | import { VideoModel } from '../../models/video/video' | 39 | import { VideoModel } from '../../models/video/video' |
40 | import { VideoChannelModel } from '../../models/video/video-channel' | 40 | import { VideoChannelModel } from '../../models/video/video-channel' |
41 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 41 | import { VideoPlaylistModel } from '../../models/video/video-playlist' |
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 228eae109..0980ec10a 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -190,6 +190,9 @@ function customConfig (): CustomConfig { | |||
190 | }, | 190 | }, |
191 | torrents: { | 191 | torrents: { |
192 | size: CONFIG.CACHE.TORRENTS.SIZE | 192 | size: CONFIG.CACHE.TORRENTS.SIZE |
193 | }, | ||
194 | storyboards: { | ||
195 | size: CONFIG.CACHE.STORYBOARDS.SIZE | ||
193 | } | 196 | } |
194 | }, | 197 | }, |
195 | signup: { | 198 | signup: { |
@@ -239,8 +242,8 @@ function customConfig (): CustomConfig { | |||
239 | '2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p'] | 242 | '2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p'] |
240 | }, | 243 | }, |
241 | alwaysTranscodeOriginalResolution: CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION, | 244 | alwaysTranscodeOriginalResolution: CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION, |
242 | webtorrent: { | 245 | webVideos: { |
243 | enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED | 246 | enabled: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED |
244 | }, | 247 | }, |
245 | hls: { | 248 | hls: { |
246 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | 249 | enabled: CONFIG.TRANSCODING.HLS.ENABLED |
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts index 31f1a56f9..38bd135d0 100644 --- a/server/controllers/api/index.ts +++ b/server/controllers/api/index.ts | |||
@@ -1,8 +1,7 @@ | |||
1 | import cors from 'cors' | 1 | import cors from 'cors' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | 3 | import { logger } from '@server/helpers/logger' | |
4 | import { HttpStatusCode } from '../../../shared/models' | 4 | import { HttpStatusCode } from '../../../shared/models' |
5 | import { badRequest } from '../../helpers/express-utils' | ||
6 | import { abuseRouter } from './abuse' | 5 | import { abuseRouter } from './abuse' |
7 | import { accountsRouter } from './accounts' | 6 | import { accountsRouter } from './accounts' |
8 | import { blocklistRouter } from './blocklist' | 7 | import { blocklistRouter } from './blocklist' |
@@ -64,3 +63,11 @@ export { apiRouter } | |||
64 | function pong (req: express.Request, res: express.Response) { | 63 | function pong (req: express.Request, res: express.Response) { |
65 | return res.send('pong').status(HttpStatusCode.OK_200).end() | 64 | return res.send('pong').status(HttpStatusCode.OK_200).end() |
66 | } | 65 | } |
66 | |||
67 | function badRequest (req: express.Request, res: express.Response) { | ||
68 | logger.debug(`API express handler not found: bad PeerTube request for ${req.method} - ${req.originalUrl}`) | ||
69 | |||
70 | return res.type('json') | ||
71 | .status(HttpStatusCode.BAD_REQUEST_400) | ||
72 | .end() | ||
73 | } | ||
diff --git a/server/controllers/api/runners/jobs-files.ts b/server/controllers/api/runners/jobs-files.ts index 4e69fb902..cb4eff570 100644 --- a/server/controllers/api/runners/jobs-files.ts +++ b/server/controllers/api/runners/jobs-files.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 2 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
3 | import { proxifyHLS, proxifyWebTorrentFile } from '@server/lib/object-storage' | 3 | import { proxifyHLS, proxifyWebVideoFile } from '@server/lib/object-storage' |
4 | import { VideoPathManager } from '@server/lib/video-path-manager' | 4 | import { VideoPathManager } from '@server/lib/video-path-manager' |
5 | import { getStudioTaskFilePath } from '@server/lib/video-studio' | 5 | import { getStudioTaskFilePath } from '@server/lib/video-studio' |
6 | import { apiRateLimiter, asyncMiddleware } from '@server/middlewares' | 6 | import { apiRateLimiter, asyncMiddleware } from '@server/middlewares' |
@@ -70,7 +70,7 @@ async function getMaxQualityVideoFile (req: express.Request, res: express.Respon | |||
70 | } | 70 | } |
71 | 71 | ||
72 | // Web video | 72 | // Web video |
73 | return proxifyWebTorrentFile({ | 73 | return proxifyWebVideoFile({ |
74 | req, | 74 | req, |
75 | res, | 75 | res, |
76 | filename: file.filename | 76 | filename: file.filename |
diff --git a/server/controllers/api/search/search-videos.ts b/server/controllers/api/search/search-videos.ts index 1d7a7b7bc..034a63ace 100644 --- a/server/controllers/api/search/search-videos.ts +++ b/server/controllers/api/search/search-videos.ts | |||
@@ -8,7 +8,6 @@ import { getOrCreateAPVideo } from '@server/lib/activitypub/videos' | |||
8 | import { Hooks } from '@server/lib/plugins/hooks' | 8 | import { Hooks } from '@server/lib/plugins/hooks' |
9 | import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' | 9 | import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' |
10 | import { getServerActor } from '@server/models/application/application' | 10 | import { getServerActor } from '@server/models/application/application' |
11 | import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' | ||
12 | import { HttpStatusCode, ResultList, Video } from '@shared/models' | 11 | import { HttpStatusCode, ResultList, Video } from '@shared/models' |
13 | import { VideosSearchQueryAfterSanitize } from '../../../../shared/models/search' | 12 | import { VideosSearchQueryAfterSanitize } from '../../../../shared/models/search' |
14 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' | 13 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' |
@@ -25,6 +24,7 @@ import { | |||
25 | videosSearchSortValidator, | 24 | videosSearchSortValidator, |
26 | videosSearchValidator | 25 | videosSearchValidator |
27 | } from '../../../middlewares' | 26 | } from '../../../middlewares' |
27 | import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter' | ||
28 | import { VideoModel } from '../../../models/video/video' | 28 | import { VideoModel } from '../../../models/video/video' |
29 | import { MVideoAccountLightBlacklistAllFiles } from '../../../types/models' | 29 | import { MVideoAccountLightBlacklistAllFiles } from '../../../types/models' |
30 | import { searchLocalUrl } from './shared' | 30 | import { searchLocalUrl } from './shared' |
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 218091d91..4753308e8 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -213,19 +213,14 @@ async function updateMe (req: express.Request, res: express.Response) { | |||
213 | 'noInstanceConfigWarningModal', | 213 | 'noInstanceConfigWarningModal', |
214 | 'noAccountSetupWarningModal', | 214 | 'noAccountSetupWarningModal', |
215 | 'noWelcomeModal', | 215 | 'noWelcomeModal', |
216 | 'emailPublic' | 216 | 'emailPublic', |
217 | 'p2pEnabled' | ||
217 | ] | 218 | ] |
218 | 219 | ||
219 | for (const key of keysToUpdate) { | 220 | for (const key of keysToUpdate) { |
220 | if (body[key] !== undefined) user.set(key, body[key]) | 221 | if (body[key] !== undefined) user.set(key, body[key]) |
221 | } | 222 | } |
222 | 223 | ||
223 | if (body.p2pEnabled !== undefined) { | ||
224 | user.set('p2pEnabled', body.p2pEnabled) | ||
225 | } else if (body.webTorrentEnabled !== undefined) { // FIXME: deprecated in 4.1 | ||
226 | user.set('p2pEnabled', body.webTorrentEnabled) | ||
227 | } | ||
228 | |||
229 | if (body.email !== undefined) { | 224 | if (body.email !== undefined) { |
230 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { | 225 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { |
231 | user.pendingEmail = body.email | 226 | user.pendingEmail = body.email |
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts index 6e2aa3711..c4360f59d 100644 --- a/server/controllers/api/users/my-subscriptions.ts +++ b/server/controllers/api/users/my-subscriptions.ts | |||
@@ -3,7 +3,7 @@ import express from 'express' | |||
3 | import { handlesToNameAndHost } from '@server/helpers/actors' | 3 | import { handlesToNameAndHost } from '@server/helpers/actors' |
4 | import { pickCommonVideoQuery } from '@server/helpers/query' | 4 | import { pickCommonVideoQuery } from '@server/helpers/query' |
5 | import { sendUndoFollow } from '@server/lib/activitypub/send' | 5 | import { sendUndoFollow } from '@server/lib/activitypub/send' |
6 | import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' | 6 | import { Hooks } from '@server/lib/plugins/hooks' |
7 | import { VideoChannelModel } from '@server/models/video/video-channel' | 7 | import { VideoChannelModel } from '@server/models/video/video-channel' |
8 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | 8 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' |
9 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' | 9 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' |
@@ -29,8 +29,8 @@ import { | |||
29 | videosSortValidator | 29 | videosSortValidator |
30 | } from '../../../middlewares/validators' | 30 | } from '../../../middlewares/validators' |
31 | import { ActorFollowModel } from '../../../models/actor/actor-follow' | 31 | import { ActorFollowModel } from '../../../models/actor/actor-follow' |
32 | import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter' | ||
32 | import { VideoModel } from '../../../models/video/video' | 33 | import { VideoModel } from '../../../models/video/video' |
33 | import { Hooks } from '@server/lib/plugins/hooks' | ||
34 | 34 | ||
35 | const mySubscriptionsRouter = express.Router() | 35 | const mySubscriptionsRouter = express.Router() |
36 | 36 | ||
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index cdafa31dc..3d7ef31ee 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -4,7 +4,6 @@ import { getBiggestActorImage } from '@server/lib/actor-image' | |||
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { ActorFollowModel } from '@server/models/actor/actor-follow' | 5 | import { ActorFollowModel } from '@server/models/actor/actor-follow' |
6 | import { getServerActor } from '@server/models/application/application' | 6 | import { getServerActor } from '@server/models/application/application' |
7 | import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' | ||
8 | import { MChannelBannerAccountDefault } from '@server/types/models' | 7 | import { MChannelBannerAccountDefault } from '@server/types/models' |
9 | import { ActorImageType, HttpStatusCode, VideoChannelCreate, VideoChannelUpdate, VideosImportInChannelCreate } from '@shared/models' | 8 | import { ActorImageType, HttpStatusCode, VideoChannelCreate, VideoChannelUpdate, VideosImportInChannelCreate } from '@shared/models' |
10 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' | 9 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' |
@@ -48,6 +47,7 @@ import { | |||
48 | import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' | 47 | import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' |
49 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' | 48 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' |
50 | import { AccountModel } from '../../models/account/account' | 49 | import { AccountModel } from '../../models/account/account' |
50 | import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter' | ||
51 | import { VideoModel } from '../../models/video/video' | 51 | import { VideoModel } from '../../models/video/video' |
52 | import { VideoChannelModel } from '../../models/video/video-channel' | 52 | import { VideoChannelModel } from '../../models/video/video-channel' |
53 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 53 | import { VideoPlaylistModel } from '../../models/video/video-playlist' |
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts index fe00034ed..73362e1e3 100644 --- a/server/controllers/api/video-playlist.ts +++ b/server/controllers/api/video-playlist.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { join } from 'path' | ||
3 | import { scheduleRefreshIfNeeded } from '@server/lib/activitypub/playlists' | 2 | import { scheduleRefreshIfNeeded } from '@server/lib/activitypub/playlists' |
3 | import { VideoMiniaturePermanentFileCache } from '@server/lib/files-cache' | ||
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
6 | import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/types/models' | 6 | import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/types/models' |
@@ -18,12 +18,11 @@ import { resetSequelizeInstance } from '../../helpers/database-utils' | |||
18 | import { createReqFiles } from '../../helpers/express-utils' | 18 | import { createReqFiles } from '../../helpers/express-utils' |
19 | import { logger } from '../../helpers/logger' | 19 | import { logger } from '../../helpers/logger' |
20 | import { getFormattedObjects } from '../../helpers/utils' | 20 | import { getFormattedObjects } from '../../helpers/utils' |
21 | import { CONFIG } from '../../initializers/config' | ||
22 | import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants' | 21 | import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants' |
23 | import { sequelizeTypescript } from '../../initializers/database' | 22 | import { sequelizeTypescript } from '../../initializers/database' |
24 | import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' | 23 | import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' |
25 | import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' | 24 | import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' |
26 | import { updatePlaylistMiniatureFromExisting } from '../../lib/thumbnail' | 25 | import { updateLocalPlaylistMiniatureFromExisting } from '../../lib/thumbnail' |
27 | import { | 26 | import { |
28 | apiRateLimiter, | 27 | apiRateLimiter, |
29 | asyncMiddleware, | 28 | asyncMiddleware, |
@@ -178,7 +177,7 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) { | |||
178 | 177 | ||
179 | const thumbnailField = req.files['thumbnailfile'] | 178 | const thumbnailField = req.files['thumbnailfile'] |
180 | const thumbnailModel = thumbnailField | 179 | const thumbnailModel = thumbnailField |
181 | ? await updatePlaylistMiniatureFromExisting({ | 180 | ? await updateLocalPlaylistMiniatureFromExisting({ |
182 | inputPath: thumbnailField[0].path, | 181 | inputPath: thumbnailField[0].path, |
183 | playlist: videoPlaylist, | 182 | playlist: videoPlaylist, |
184 | automaticallyGenerated: false | 183 | automaticallyGenerated: false |
@@ -220,7 +219,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response) | |||
220 | 219 | ||
221 | const thumbnailField = req.files['thumbnailfile'] | 220 | const thumbnailField = req.files['thumbnailfile'] |
222 | const thumbnailModel = thumbnailField | 221 | const thumbnailModel = thumbnailField |
223 | ? await updatePlaylistMiniatureFromExisting({ | 222 | ? await updateLocalPlaylistMiniatureFromExisting({ |
224 | inputPath: thumbnailField[0].path, | 223 | inputPath: thumbnailField[0].path, |
225 | playlist: videoPlaylistInstance, | 224 | playlist: videoPlaylistInstance, |
226 | automaticallyGenerated: false | 225 | automaticallyGenerated: false |
@@ -496,8 +495,13 @@ async function generateThumbnailForPlaylist (videoPlaylist: MVideoPlaylistThumbn | |||
496 | return | 495 | return |
497 | } | 496 | } |
498 | 497 | ||
499 | const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoMiniature.filename) | 498 | // Ensure the file is on disk |
500 | const thumbnailModel = await updatePlaylistMiniatureFromExisting({ | 499 | const videoMiniaturePermanentFileCache = new VideoMiniaturePermanentFileCache() |
500 | const inputPath = videoMiniature.isOwned() | ||
501 | ? videoMiniature.getPath() | ||
502 | : await videoMiniaturePermanentFileCache.downloadRemoteFile(videoMiniature) | ||
503 | |||
504 | const thumbnailModel = await updateLocalPlaylistMiniatureFromExisting({ | ||
501 | inputPath, | 505 | inputPath, |
502 | playlist: videoPlaylist, | 506 | playlist: videoPlaylist, |
503 | automaticallyGenerated: true, | 507 | automaticallyGenerated: true, |
diff --git a/server/controllers/api/videos/files.ts b/server/controllers/api/videos/files.ts index 6d9c0b843..67b60ff63 100644 --- a/server/controllers/api/videos/files.ts +++ b/server/controllers/api/videos/files.ts | |||
@@ -2,7 +2,8 @@ import express from 'express' | |||
2 | import toInt from 'validator/lib/toInt' | 2 | import toInt from 'validator/lib/toInt' |
3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
4 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | 4 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' |
5 | import { removeAllWebTorrentFiles, removeHLSFile, removeHLSPlaylist, removeWebTorrentFile } from '@server/lib/video-file' | 5 | import { updatePlaylistAfterFileChange } from '@server/lib/hls' |
6 | import { removeAllWebVideoFiles, removeHLSFile, removeHLSPlaylist, removeWebVideoFile } from '@server/lib/video-file' | ||
6 | import { VideoFileModel } from '@server/models/video/video-file' | 7 | import { VideoFileModel } from '@server/models/video/video-file' |
7 | import { HttpStatusCode, UserRight } from '@shared/models' | 8 | import { HttpStatusCode, UserRight } from '@shared/models' |
8 | import { | 9 | import { |
@@ -12,11 +13,10 @@ import { | |||
12 | videoFileMetadataGetValidator, | 13 | videoFileMetadataGetValidator, |
13 | videoFilesDeleteHLSFileValidator, | 14 | videoFilesDeleteHLSFileValidator, |
14 | videoFilesDeleteHLSValidator, | 15 | videoFilesDeleteHLSValidator, |
15 | videoFilesDeleteWebTorrentFileValidator, | 16 | videoFilesDeleteWebVideoFileValidator, |
16 | videoFilesDeleteWebTorrentValidator, | 17 | videoFilesDeleteWebVideoValidator, |
17 | videosGetValidator | 18 | videosGetValidator |
18 | } from '../../../middlewares' | 19 | } from '../../../middlewares' |
19 | import { updatePlaylistAfterFileChange } from '@server/lib/hls' | ||
20 | 20 | ||
21 | const lTags = loggerTagsFactory('api', 'video') | 21 | const lTags = loggerTagsFactory('api', 'video') |
22 | const filesRouter = express.Router() | 22 | const filesRouter = express.Router() |
@@ -40,17 +40,19 @@ filesRouter.delete('/:id/hls/:videoFileId', | |||
40 | asyncMiddleware(removeHLSFileController) | 40 | asyncMiddleware(removeHLSFileController) |
41 | ) | 41 | ) |
42 | 42 | ||
43 | filesRouter.delete('/:id/webtorrent', | 43 | filesRouter.delete( |
44 | [ '/:id/webtorrent', '/:id/web-videos' ], // TODO: remove webtorrent in V7 | ||
44 | authenticate, | 45 | authenticate, |
45 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), | 46 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), |
46 | asyncMiddleware(videoFilesDeleteWebTorrentValidator), | 47 | asyncMiddleware(videoFilesDeleteWebVideoValidator), |
47 | asyncMiddleware(removeAllWebTorrentFilesController) | 48 | asyncMiddleware(removeAllWebVideoFilesController) |
48 | ) | 49 | ) |
49 | filesRouter.delete('/:id/webtorrent/:videoFileId', | 50 | filesRouter.delete( |
51 | [ '/:id/webtorrent/:videoFileId', '/:id/web-videos/:videoFileId' ], // TODO: remove webtorrent in V7 | ||
50 | authenticate, | 52 | authenticate, |
51 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), | 53 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), |
52 | asyncMiddleware(videoFilesDeleteWebTorrentFileValidator), | 54 | asyncMiddleware(videoFilesDeleteWebVideoFileValidator), |
53 | asyncMiddleware(removeWebTorrentFileController) | 55 | asyncMiddleware(removeWebVideoFileController) |
54 | ) | 56 | ) |
55 | 57 | ||
56 | // --------------------------------------------------------------------------- | 58 | // --------------------------------------------------------------------------- |
@@ -96,24 +98,24 @@ async function removeHLSFileController (req: express.Request, res: express.Respo | |||
96 | 98 | ||
97 | // --------------------------------------------------------------------------- | 99 | // --------------------------------------------------------------------------- |
98 | 100 | ||
99 | async function removeAllWebTorrentFilesController (req: express.Request, res: express.Response) { | 101 | async function removeAllWebVideoFilesController (req: express.Request, res: express.Response) { |
100 | const video = res.locals.videoAll | 102 | const video = res.locals.videoAll |
101 | 103 | ||
102 | logger.info('Deleting WebTorrent files of %s.', video.url, lTags(video.uuid)) | 104 | logger.info('Deleting Web Video files of %s.', video.url, lTags(video.uuid)) |
103 | 105 | ||
104 | await removeAllWebTorrentFiles(video) | 106 | await removeAllWebVideoFiles(video) |
105 | await federateVideoIfNeeded(video, false, undefined) | 107 | await federateVideoIfNeeded(video, false, undefined) |
106 | 108 | ||
107 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 109 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) |
108 | } | 110 | } |
109 | 111 | ||
110 | async function removeWebTorrentFileController (req: express.Request, res: express.Response) { | 112 | async function removeWebVideoFileController (req: express.Request, res: express.Response) { |
111 | const video = res.locals.videoAll | 113 | const video = res.locals.videoAll |
112 | 114 | ||
113 | const videoFileId = +req.params.videoFileId | 115 | const videoFileId = +req.params.videoFileId |
114 | logger.info('Deleting WebTorrent file %d of %s.', videoFileId, video.url, lTags(video.uuid)) | 116 | logger.info('Deleting Web Video file %d of %s.', videoFileId, video.url, lTags(video.uuid)) |
115 | 117 | ||
116 | await removeWebTorrentFile(video, videoFileId) | 118 | await removeWebVideoFile(video, videoFileId) |
117 | await federateVideoIfNeeded(video, false, undefined) | 119 | await federateVideoIfNeeded(video, false, undefined) |
118 | 120 | ||
119 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 121 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) |
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index 6a50aaf4e..defe9efd4 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts | |||
@@ -14,7 +14,7 @@ import { getSecureTorrentName } from '../../../helpers/utils' | |||
14 | import { CONFIG } from '../../../initializers/config' | 14 | import { CONFIG } from '../../../initializers/config' |
15 | import { MIMETYPES } from '../../../initializers/constants' | 15 | import { MIMETYPES } from '../../../initializers/constants' |
16 | import { JobQueue } from '../../../lib/job-queue/job-queue' | 16 | import { JobQueue } from '../../../lib/job-queue/job-queue' |
17 | import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' | 17 | import { updateLocalVideoMiniatureFromExisting } from '../../../lib/thumbnail' |
18 | import { | 18 | import { |
19 | asyncMiddleware, | 19 | asyncMiddleware, |
20 | asyncRetryTransactionMiddleware, | 20 | asyncRetryTransactionMiddleware, |
@@ -120,6 +120,7 @@ async function handleTorrentImport (req: express.Request, res: express.Response, | |||
120 | videoChannel: res.locals.videoChannel, | 120 | videoChannel: res.locals.videoChannel, |
121 | tags: body.tags || undefined, | 121 | tags: body.tags || undefined, |
122 | user, | 122 | user, |
123 | videoPasswords: body.videoPasswords, | ||
123 | videoImportAttributes: { | 124 | videoImportAttributes: { |
124 | magnetUri, | 125 | magnetUri, |
125 | torrentName, | 126 | torrentName, |
@@ -192,7 +193,7 @@ async function processThumbnail (req: express.Request, video: MVideoThumbnail) { | |||
192 | if (thumbnailField) { | 193 | if (thumbnailField) { |
193 | const thumbnailPhysicalFile = thumbnailField[0] | 194 | const thumbnailPhysicalFile = thumbnailField[0] |
194 | 195 | ||
195 | return updateVideoMiniatureFromExisting({ | 196 | return updateLocalVideoMiniatureFromExisting({ |
196 | inputPath: thumbnailPhysicalFile.path, | 197 | inputPath: thumbnailPhysicalFile.path, |
197 | video, | 198 | video, |
198 | type: ThumbnailType.MINIATURE, | 199 | type: ThumbnailType.MINIATURE, |
@@ -208,7 +209,7 @@ async function processPreview (req: express.Request, video: MVideoThumbnail): Pr | |||
208 | if (previewField) { | 209 | if (previewField) { |
209 | const previewPhysicalFile = previewField[0] | 210 | const previewPhysicalFile = previewField[0] |
210 | 211 | ||
211 | return updateVideoMiniatureFromExisting({ | 212 | return updateLocalVideoMiniatureFromExisting({ |
212 | inputPath: previewPhysicalFile.path, | 213 | inputPath: previewPhysicalFile.path, |
213 | video, | 214 | video, |
214 | type: ThumbnailType.PREVIEW, | 215 | type: ThumbnailType.PREVIEW, |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index a34325e79..520d8cbbb 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -3,7 +3,6 @@ import { pickCommonVideoQuery } from '@server/helpers/query' | |||
3 | import { doJSONRequest } from '@server/helpers/requests' | 3 | import { doJSONRequest } from '@server/helpers/requests' |
4 | import { openapiOperationDoc } from '@server/middlewares/doc' | 4 | import { openapiOperationDoc } from '@server/middlewares/doc' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
6 | import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' | ||
7 | import { MVideoAccountLight } from '@server/types/models' | 6 | import { MVideoAccountLight } from '@server/types/models' |
8 | import { HttpStatusCode } from '../../../../shared/models' | 7 | import { HttpStatusCode } from '../../../../shared/models' |
9 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 8 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
@@ -31,6 +30,7 @@ import { | |||
31 | videosRemoveValidator, | 30 | videosRemoveValidator, |
32 | videosSortValidator | 31 | videosSortValidator |
33 | } from '../../../middlewares' | 32 | } from '../../../middlewares' |
33 | import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter' | ||
34 | import { VideoModel } from '../../../models/video/video' | 34 | import { VideoModel } from '../../../models/video/video' |
35 | import { blacklistRouter } from './blacklist' | 35 | import { blacklistRouter } from './blacklist' |
36 | import { videoCaptionsRouter } from './captions' | 36 | import { videoCaptionsRouter } from './captions' |
@@ -41,12 +41,14 @@ import { liveRouter } from './live' | |||
41 | import { ownershipVideoRouter } from './ownership' | 41 | import { ownershipVideoRouter } from './ownership' |
42 | import { rateVideoRouter } from './rate' | 42 | import { rateVideoRouter } from './rate' |
43 | import { statsRouter } from './stats' | 43 | import { statsRouter } from './stats' |
44 | import { storyboardRouter } from './storyboard' | ||
44 | import { studioRouter } from './studio' | 45 | import { studioRouter } from './studio' |
45 | import { tokenRouter } from './token' | 46 | import { tokenRouter } from './token' |
46 | import { transcodingRouter } from './transcoding' | 47 | import { transcodingRouter } from './transcoding' |
47 | import { updateRouter } from './update' | 48 | import { updateRouter } from './update' |
48 | import { uploadRouter } from './upload' | 49 | import { uploadRouter } from './upload' |
49 | import { viewRouter } from './view' | 50 | import { viewRouter } from './view' |
51 | import { videoPasswordRouter } from './passwords' | ||
50 | 52 | ||
51 | const auditLogger = auditLoggerFactory('videos') | 53 | const auditLogger = auditLoggerFactory('videos') |
52 | const videosRouter = express.Router() | 54 | const videosRouter = express.Router() |
@@ -68,6 +70,8 @@ videosRouter.use('/', updateRouter) | |||
68 | videosRouter.use('/', filesRouter) | 70 | videosRouter.use('/', filesRouter) |
69 | videosRouter.use('/', transcodingRouter) | 71 | videosRouter.use('/', transcodingRouter) |
70 | videosRouter.use('/', tokenRouter) | 72 | videosRouter.use('/', tokenRouter) |
73 | videosRouter.use('/', videoPasswordRouter) | ||
74 | videosRouter.use('/', storyboardRouter) | ||
71 | 75 | ||
72 | videosRouter.get('/categories', | 76 | videosRouter.get('/categories', |
73 | openapiOperationDoc({ operationId: 'getCategories' }), | 77 | openapiOperationDoc({ operationId: 'getCategories' }), |
diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts index de047d4ec..e19e8c652 100644 --- a/server/controllers/api/videos/live.ts +++ b/server/controllers/api/videos/live.ts | |||
@@ -18,13 +18,14 @@ import { VideoLiveModel } from '@server/models/video/video-live' | |||
18 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' | 18 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' |
19 | import { MVideoDetails, MVideoFullLight, MVideoLive } from '@server/types/models' | 19 | import { MVideoDetails, MVideoFullLight, MVideoLive } from '@server/types/models' |
20 | import { buildUUID, uuidToShort } from '@shared/extra-utils' | 20 | import { buildUUID, uuidToShort } from '@shared/extra-utils' |
21 | import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoState } from '@shared/models' | 21 | import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoPrivacy, VideoState } from '@shared/models' |
22 | import { logger } from '../../../helpers/logger' | 22 | import { logger } from '../../../helpers/logger' |
23 | import { sequelizeTypescript } from '../../../initializers/database' | 23 | import { sequelizeTypescript } from '../../../initializers/database' |
24 | import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' | 24 | import { updateLocalVideoMiniatureFromExisting } from '../../../lib/thumbnail' |
25 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares' | 25 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares' |
26 | import { VideoModel } from '../../../models/video/video' | 26 | import { VideoModel } from '../../../models/video/video' |
27 | import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' | 27 | import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' |
28 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
28 | 29 | ||
29 | const liveRouter = express.Router() | 30 | const liveRouter = express.Router() |
30 | 31 | ||
@@ -165,7 +166,7 @@ async function addLiveVideo (req: express.Request, res: express.Response) { | |||
165 | video, | 166 | video, |
166 | files: req.files, | 167 | files: req.files, |
167 | fallback: type => { | 168 | fallback: type => { |
168 | return updateVideoMiniatureFromExisting({ | 169 | return updateLocalVideoMiniatureFromExisting({ |
169 | inputPath: ASSETS_PATH.DEFAULT_LIVE_BACKGROUND, | 170 | inputPath: ASSETS_PATH.DEFAULT_LIVE_BACKGROUND, |
170 | video, | 171 | video, |
171 | type, | 172 | type, |
@@ -202,6 +203,10 @@ async function addLiveVideo (req: express.Request, res: express.Response) { | |||
202 | 203 | ||
203 | await federateVideoIfNeeded(videoCreated, true, t) | 204 | await federateVideoIfNeeded(videoCreated, true, t) |
204 | 205 | ||
206 | if (videoInfo.privacy === VideoPrivacy.PASSWORD_PROTECTED) { | ||
207 | await VideoPasswordModel.addPasswords(videoInfo.videoPasswords, video.id, t) | ||
208 | } | ||
209 | |||
205 | logger.info('Video live %s with uuid %s created.', videoInfo.name, videoCreated.uuid) | 210 | logger.info('Video live %s with uuid %s created.', videoInfo.name, videoCreated.uuid) |
206 | 211 | ||
207 | return { videoCreated } | 212 | return { videoCreated } |
diff --git a/server/controllers/api/videos/passwords.ts b/server/controllers/api/videos/passwords.ts new file mode 100644 index 000000000..d11cf5bcc --- /dev/null +++ b/server/controllers/api/videos/passwords.ts | |||
@@ -0,0 +1,105 @@ | |||
1 | import express from 'express' | ||
2 | |||
3 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | ||
4 | import { getFormattedObjects } from '../../../helpers/utils' | ||
5 | import { | ||
6 | asyncMiddleware, | ||
7 | asyncRetryTransactionMiddleware, | ||
8 | authenticate, | ||
9 | setDefaultPagination, | ||
10 | setDefaultSort | ||
11 | } from '../../../middlewares' | ||
12 | import { | ||
13 | listVideoPasswordValidator, | ||
14 | paginationValidator, | ||
15 | removeVideoPasswordValidator, | ||
16 | updateVideoPasswordListValidator, | ||
17 | videoPasswordsSortValidator | ||
18 | } from '../../../middlewares/validators' | ||
19 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
20 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
21 | import { Transaction } from 'sequelize' | ||
22 | import { getVideoWithAttributes } from '@server/helpers/video' | ||
23 | |||
24 | const lTags = loggerTagsFactory('api', 'video') | ||
25 | const videoPasswordRouter = express.Router() | ||
26 | |||
27 | videoPasswordRouter.get('/:videoId/passwords', | ||
28 | authenticate, | ||
29 | paginationValidator, | ||
30 | videoPasswordsSortValidator, | ||
31 | setDefaultSort, | ||
32 | setDefaultPagination, | ||
33 | asyncMiddleware(listVideoPasswordValidator), | ||
34 | asyncMiddleware(listVideoPasswords) | ||
35 | ) | ||
36 | |||
37 | videoPasswordRouter.put('/:videoId/passwords', | ||
38 | authenticate, | ||
39 | asyncMiddleware(updateVideoPasswordListValidator), | ||
40 | asyncMiddleware(updateVideoPasswordList) | ||
41 | ) | ||
42 | |||
43 | videoPasswordRouter.delete('/:videoId/passwords/:passwordId', | ||
44 | authenticate, | ||
45 | asyncMiddleware(removeVideoPasswordValidator), | ||
46 | asyncRetryTransactionMiddleware(removeVideoPassword) | ||
47 | ) | ||
48 | |||
49 | // --------------------------------------------------------------------------- | ||
50 | |||
51 | export { | ||
52 | videoPasswordRouter | ||
53 | } | ||
54 | |||
55 | // --------------------------------------------------------------------------- | ||
56 | |||
57 | async function listVideoPasswords (req: express.Request, res: express.Response) { | ||
58 | const options = { | ||
59 | videoId: res.locals.videoAll.id, | ||
60 | start: req.query.start, | ||
61 | count: req.query.count, | ||
62 | sort: req.query.sort | ||
63 | } | ||
64 | |||
65 | const resultList = await VideoPasswordModel.listPasswords(options) | ||
66 | |||
67 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
68 | } | ||
69 | |||
70 | async function updateVideoPasswordList (req: express.Request, res: express.Response) { | ||
71 | const videoInstance = getVideoWithAttributes(res) | ||
72 | const videoId = videoInstance.id | ||
73 | |||
74 | const passwordArray = req.body.passwords as string[] | ||
75 | |||
76 | await VideoPasswordModel.sequelize.transaction(async (t: Transaction) => { | ||
77 | await VideoPasswordModel.deleteAllPasswords(videoId, t) | ||
78 | await VideoPasswordModel.addPasswords(passwordArray, videoId, t) | ||
79 | }) | ||
80 | |||
81 | logger.info( | ||
82 | `Video passwords for video with name %s and uuid %s have been updated`, | ||
83 | videoInstance.name, | ||
84 | videoInstance.uuid, | ||
85 | lTags(videoInstance.uuid) | ||
86 | ) | ||
87 | |||
88 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
89 | } | ||
90 | |||
91 | async function removeVideoPassword (req: express.Request, res: express.Response) { | ||
92 | const videoInstance = getVideoWithAttributes(res) | ||
93 | const password = res.locals.videoPassword | ||
94 | |||
95 | await VideoPasswordModel.deletePassword(password.id) | ||
96 | logger.info( | ||
97 | 'Password with id %d of video named %s and uuid %s has been deleted.', | ||
98 | password.id, | ||
99 | videoInstance.name, | ||
100 | videoInstance.uuid, | ||
101 | lTags(videoInstance.uuid) | ||
102 | ) | ||
103 | |||
104 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
105 | } | ||
diff --git a/server/controllers/api/videos/storyboard.ts b/server/controllers/api/videos/storyboard.ts new file mode 100644 index 000000000..47a22011d --- /dev/null +++ b/server/controllers/api/videos/storyboard.ts | |||
@@ -0,0 +1,29 @@ | |||
1 | import express from 'express' | ||
2 | import { getVideoWithAttributes } from '@server/helpers/video' | ||
3 | import { StoryboardModel } from '@server/models/video/storyboard' | ||
4 | import { asyncMiddleware, videosGetValidator } from '../../../middlewares' | ||
5 | |||
6 | const storyboardRouter = express.Router() | ||
7 | |||
8 | storyboardRouter.get('/:id/storyboards', | ||
9 | asyncMiddleware(videosGetValidator), | ||
10 | asyncMiddleware(listStoryboards) | ||
11 | ) | ||
12 | |||
13 | // --------------------------------------------------------------------------- | ||
14 | |||
15 | export { | ||
16 | storyboardRouter | ||
17 | } | ||
18 | |||
19 | // --------------------------------------------------------------------------- | ||
20 | |||
21 | async function listStoryboards (req: express.Request, res: express.Response) { | ||
22 | const video = getVideoWithAttributes(res) | ||
23 | |||
24 | const storyboards = await StoryboardModel.listStoryboardsOf(video) | ||
25 | |||
26 | return res.json({ | ||
27 | storyboards: storyboards.map(s => s.toFormattedJSON()) | ||
28 | }) | ||
29 | } | ||
diff --git a/server/controllers/api/videos/token.ts b/server/controllers/api/videos/token.ts index 22387c3e8..e961ffd9e 100644 --- a/server/controllers/api/videos/token.ts +++ b/server/controllers/api/videos/token.ts | |||
@@ -1,13 +1,14 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { VideoTokensManager } from '@server/lib/video-tokens-manager' | 2 | import { VideoTokensManager } from '@server/lib/video-tokens-manager' |
3 | import { VideoToken } from '@shared/models' | 3 | import { VideoPrivacy, VideoToken } from '@shared/models' |
4 | import { asyncMiddleware, authenticate, videosCustomGetValidator } from '../../../middlewares' | 4 | import { asyncMiddleware, optionalAuthenticate, videoFileTokenValidator, videosCustomGetValidator } from '../../../middlewares' |
5 | 5 | ||
6 | const tokenRouter = express.Router() | 6 | const tokenRouter = express.Router() |
7 | 7 | ||
8 | tokenRouter.post('/:id/token', | 8 | tokenRouter.post('/:id/token', |
9 | authenticate, | 9 | optionalAuthenticate, |
10 | asyncMiddleware(videosCustomGetValidator('only-video')), | 10 | asyncMiddleware(videosCustomGetValidator('only-video')), |
11 | videoFileTokenValidator, | ||
11 | generateToken | 12 | generateToken |
12 | ) | 13 | ) |
13 | 14 | ||
@@ -22,12 +23,11 @@ export { | |||
22 | function generateToken (req: express.Request, res: express.Response) { | 23 | function generateToken (req: express.Request, res: express.Response) { |
23 | const video = res.locals.onlyVideo | 24 | const video = res.locals.onlyVideo |
24 | 25 | ||
25 | const { token, expires } = VideoTokensManager.Instance.create({ videoUUID: video.uuid, user: res.locals.oauth.token.User }) | 26 | const files = video.privacy === VideoPrivacy.PASSWORD_PROTECTED |
27 | ? VideoTokensManager.Instance.createForPasswordProtectedVideo({ videoUUID: video.uuid }) | ||
28 | : VideoTokensManager.Instance.createForAuthUser({ videoUUID: video.uuid, user: res.locals.oauth.token.User }) | ||
26 | 29 | ||
27 | return res.json({ | 30 | return res.json({ |
28 | files: { | 31 | files |
29 | token, | ||
30 | expires | ||
31 | } | ||
32 | } as VideoToken) | 32 | } as VideoToken) |
33 | } | 33 | } |
diff --git a/server/controllers/api/videos/update.ts b/server/controllers/api/videos/update.ts index ddab428d4..28ec2cf37 100644 --- a/server/controllers/api/videos/update.ts +++ b/server/controllers/api/videos/update.ts | |||
@@ -2,13 +2,12 @@ import express from 'express' | |||
2 | import { Transaction } from 'sequelize/types' | 2 | import { Transaction } from 'sequelize/types' |
3 | import { changeVideoChannelShare } from '@server/lib/activitypub/share' | 3 | import { changeVideoChannelShare } from '@server/lib/activitypub/share' |
4 | import { addVideoJobsAfterUpdate, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | 4 | import { addVideoJobsAfterUpdate, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' |
5 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
6 | import { setVideoPrivacy } from '@server/lib/video-privacy' | 5 | import { setVideoPrivacy } from '@server/lib/video-privacy' |
7 | import { openapiOperationDoc } from '@server/middlewares/doc' | 6 | import { openapiOperationDoc } from '@server/middlewares/doc' |
8 | import { FilteredModelAttributes } from '@server/types' | 7 | import { FilteredModelAttributes } from '@server/types' |
9 | import { MVideoFullLight } from '@server/types/models' | 8 | import { MVideoFullLight } from '@server/types/models' |
10 | import { forceNumber } from '@shared/core-utils' | 9 | import { forceNumber } from '@shared/core-utils' |
11 | import { HttpStatusCode, VideoUpdate } from '@shared/models' | 10 | import { HttpStatusCode, VideoPrivacy, VideoUpdate } from '@shared/models' |
12 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 11 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
13 | import { resetSequelizeInstance } from '../../../helpers/database-utils' | 12 | import { resetSequelizeInstance } from '../../../helpers/database-utils' |
14 | import { createReqFiles } from '../../../helpers/express-utils' | 13 | import { createReqFiles } from '../../../helpers/express-utils' |
@@ -20,6 +19,9 @@ import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | |||
20 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares' | 19 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares' |
21 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | 20 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' |
22 | import { VideoModel } from '../../../models/video/video' | 21 | import { VideoModel } from '../../../models/video/video' |
22 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
23 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
24 | import { exists } from '@server/helpers/custom-validators/misc' | ||
23 | 25 | ||
24 | const lTags = loggerTagsFactory('api', 'video') | 26 | const lTags = loggerTagsFactory('api', 'video') |
25 | const auditLogger = auditLoggerFactory('videos') | 27 | const auditLogger = auditLoggerFactory('videos') |
@@ -176,6 +178,16 @@ async function updateVideoPrivacy (options: { | |||
176 | const newPrivacy = forceNumber(videoInfoToUpdate.privacy) | 178 | const newPrivacy = forceNumber(videoInfoToUpdate.privacy) |
177 | setVideoPrivacy(videoInstance, newPrivacy) | 179 | setVideoPrivacy(videoInstance, newPrivacy) |
178 | 180 | ||
181 | // Delete passwords if video is not anymore password protected | ||
182 | if (videoInstance.privacy === VideoPrivacy.PASSWORD_PROTECTED && newPrivacy !== VideoPrivacy.PASSWORD_PROTECTED) { | ||
183 | await VideoPasswordModel.deleteAllPasswords(videoInstance.id, transaction) | ||
184 | } | ||
185 | |||
186 | if (newPrivacy === VideoPrivacy.PASSWORD_PROTECTED && exists(videoInfoToUpdate.videoPasswords)) { | ||
187 | await VideoPasswordModel.deleteAllPasswords(videoInstance.id, transaction) | ||
188 | await VideoPasswordModel.addPasswords(videoInfoToUpdate.videoPasswords, videoInstance.id, transaction) | ||
189 | } | ||
190 | |||
179 | // Unfederate the video if the new privacy is not compatible with federation | 191 | // Unfederate the video if the new privacy is not compatible with federation |
180 | if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) { | 192 | if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) { |
181 | await VideoModel.sendDelete(videoInstance, { transaction }) | 193 | await VideoModel.sendDelete(videoInstance, { transaction }) |
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts index 885ac8b81..27fef0b1a 100644 --- a/server/controllers/api/videos/upload.ts +++ b/server/controllers/api/videos/upload.ts | |||
@@ -14,14 +14,14 @@ import { openapiOperationDoc } from '@server/middlewares/doc' | |||
14 | import { VideoSourceModel } from '@server/models/video/video-source' | 14 | import { VideoSourceModel } from '@server/models/video/video-source' |
15 | import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models' | 15 | import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models' |
16 | import { uuidToShort } from '@shared/extra-utils' | 16 | import { uuidToShort } from '@shared/extra-utils' |
17 | import { HttpStatusCode, VideoCreate, VideoState } from '@shared/models' | 17 | import { HttpStatusCode, VideoCreate, VideoPrivacy, VideoState } from '@shared/models' |
18 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 18 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
19 | import { createReqFiles } from '../../../helpers/express-utils' | 19 | import { createReqFiles } from '../../../helpers/express-utils' |
20 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | 20 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
21 | import { MIMETYPES } from '../../../initializers/constants' | 21 | import { MIMETYPES } from '../../../initializers/constants' |
22 | import { sequelizeTypescript } from '../../../initializers/database' | 22 | import { sequelizeTypescript } from '../../../initializers/database' |
23 | import { Hooks } from '../../../lib/plugins/hooks' | 23 | import { Hooks } from '../../../lib/plugins/hooks' |
24 | import { generateVideoMiniature } from '../../../lib/thumbnail' | 24 | import { generateLocalVideoMiniature } from '../../../lib/thumbnail' |
25 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | 25 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' |
26 | import { | 26 | import { |
27 | asyncMiddleware, | 27 | asyncMiddleware, |
@@ -33,6 +33,7 @@ import { | |||
33 | } from '../../../middlewares' | 33 | } from '../../../middlewares' |
34 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | 34 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' |
35 | import { VideoModel } from '../../../models/video/video' | 35 | import { VideoModel } from '../../../models/video/video' |
36 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
36 | 37 | ||
37 | const lTags = loggerTagsFactory('api', 'video') | 38 | const lTags = loggerTagsFactory('api', 'video') |
38 | const auditLogger = auditLoggerFactory('videos') | 39 | const auditLogger = auditLoggerFactory('videos') |
@@ -62,13 +63,13 @@ uploadRouter.post('/upload-resumable', | |||
62 | authenticate, | 63 | authenticate, |
63 | reqVideoFileAddResumable, | 64 | reqVideoFileAddResumable, |
64 | asyncMiddleware(videosAddResumableInitValidator), | 65 | asyncMiddleware(videosAddResumableInitValidator), |
65 | uploadx.upload | 66 | (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end |
66 | ) | 67 | ) |
67 | 68 | ||
68 | uploadRouter.delete('/upload-resumable', | 69 | uploadRouter.delete('/upload-resumable', |
69 | authenticate, | 70 | authenticate, |
70 | asyncMiddleware(deleteUploadResumableCache), | 71 | asyncMiddleware(deleteUploadResumableCache), |
71 | uploadx.upload | 72 | (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end |
72 | ) | 73 | ) |
73 | 74 | ||
74 | uploadRouter.put('/upload-resumable', | 75 | uploadRouter.put('/upload-resumable', |
@@ -110,7 +111,7 @@ async function addVideoLegacy (req: express.Request, res: express.Response) { | |||
110 | async function addVideoResumable (req: express.Request, res: express.Response) { | 111 | async function addVideoResumable (req: express.Request, res: express.Response) { |
111 | const videoPhysicalFile = res.locals.videoFileResumable | 112 | const videoPhysicalFile = res.locals.videoFileResumable |
112 | const videoInfo = videoPhysicalFile.metadata | 113 | const videoInfo = videoPhysicalFile.metadata |
113 | const files = { previewfile: videoInfo.previewfile } | 114 | const files = { previewfile: videoInfo.previewfile, thumbnailfile: videoInfo.thumbnailfile } |
114 | 115 | ||
115 | const response = await addVideo({ req, res, videoPhysicalFile, videoInfo, files }) | 116 | const response = await addVideo({ req, res, videoPhysicalFile, videoInfo, files }) |
116 | await Redis.Instance.setUploadSession(req.query.upload_id, response) | 117 | await Redis.Instance.setUploadSession(req.query.upload_id, response) |
@@ -152,7 +153,7 @@ async function addVideo (options: { | |||
152 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | 153 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ |
153 | video, | 154 | video, |
154 | files, | 155 | files, |
155 | fallback: type => generateVideoMiniature({ video, videoFile, type }) | 156 | fallback: type => generateLocalVideoMiniature({ video, videoFile, type }) |
156 | }) | 157 | }) |
157 | 158 | ||
158 | const { videoCreated } = await sequelizeTypescript.transaction(async t => { | 159 | const { videoCreated } = await sequelizeTypescript.transaction(async t => { |
@@ -195,6 +196,10 @@ async function addVideo (options: { | |||
195 | transaction: t | 196 | transaction: t |
196 | }) | 197 | }) |
197 | 198 | ||
199 | if (videoInfo.privacy === VideoPrivacy.PASSWORD_PROTECTED) { | ||
200 | await VideoPasswordModel.addPasswords(videoInfo.videoPasswords, video.id, t) | ||
201 | } | ||
202 | |||
198 | auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) | 203 | auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) |
199 | logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid)) | 204 | logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid)) |
200 | 205 | ||
@@ -230,6 +235,15 @@ async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVide | |||
230 | }, | 235 | }, |
231 | 236 | ||
232 | { | 237 | { |
238 | type: 'generate-video-storyboard' as 'generate-video-storyboard', | ||
239 | payload: { | ||
240 | videoUUID: video.uuid, | ||
241 | // No need to federate, we process these jobs sequentially | ||
242 | federate: false | ||
243 | } | ||
244 | }, | ||
245 | |||
246 | { | ||
233 | type: 'notify', | 247 | type: 'notify', |
234 | payload: { | 248 | payload: { |
235 | action: 'new-video', | 249 | action: 'new-video', |
diff --git a/server/controllers/download.ts b/server/controllers/download.ts index d675a2d6c..4b94e34bd 100644 --- a/server/controllers/download.ts +++ b/server/controllers/download.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | import cors from 'cors' | 1 | import cors from 'cors' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | import { logger } from '@server/helpers/logger' | 3 | import { logger } from '@server/helpers/logger' |
4 | import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' | 4 | import { VideoTorrentsSimpleFileCache } from '@server/lib/files-cache' |
5 | import { generateHLSFilePresignedUrl, generateWebVideoPresignedUrl } from '@server/lib/object-storage' | ||
5 | import { Hooks } from '@server/lib/plugins/hooks' | 6 | import { Hooks } from '@server/lib/plugins/hooks' |
6 | import { VideoPathManager } from '@server/lib/video-path-manager' | 7 | import { VideoPathManager } from '@server/lib/video-path-manager' |
7 | import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 8 | import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
8 | import { addQueryParams, forceNumber } from '@shared/core-utils' | 9 | import { forceNumber } from '@shared/core-utils' |
9 | import { HttpStatusCode, VideoStorage, VideoStreamingPlaylistType } from '@shared/models' | 10 | import { HttpStatusCode, VideoStorage, VideoStreamingPlaylistType } from '@shared/models' |
10 | import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' | 11 | import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' |
11 | import { asyncMiddleware, optionalAuthenticate, videosDownloadValidator } from '../middlewares' | 12 | import { asyncMiddleware, optionalAuthenticate, videosDownloadValidator } from '../middlewares' |
@@ -42,7 +43,7 @@ export { | |||
42 | // --------------------------------------------------------------------------- | 43 | // --------------------------------------------------------------------------- |
43 | 44 | ||
44 | async function downloadTorrent (req: express.Request, res: express.Response) { | 45 | async function downloadTorrent (req: express.Request, res: express.Response) { |
45 | const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) | 46 | const result = await VideoTorrentsSimpleFileCache.Instance.getFilePath(req.params.filename) |
46 | if (!result) { | 47 | if (!result) { |
47 | return res.fail({ | 48 | return res.fail({ |
48 | status: HttpStatusCode.NOT_FOUND_404, | 49 | status: HttpStatusCode.NOT_FOUND_404, |
@@ -94,16 +95,16 @@ async function downloadVideoFile (req: express.Request, res: express.Response) { | |||
94 | 95 | ||
95 | if (!checkAllowResult(res, allowParameters, allowedResult)) return | 96 | if (!checkAllowResult(res, allowParameters, allowedResult)) return |
96 | 97 | ||
98 | // Express uses basename on filename parameter | ||
99 | const videoName = video.name.replace(/[/\\]/g, '_') | ||
100 | const downloadFilename = `${videoName}-${videoFile.resolution}p${videoFile.extname}` | ||
101 | |||
97 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { | 102 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { |
98 | return redirectToObjectStorage({ req, res, video, file: videoFile }) | 103 | return redirectToObjectStorage({ req, res, video, file: videoFile, downloadFilename }) |
99 | } | 104 | } |
100 | 105 | ||
101 | await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), path => { | 106 | await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), path => { |
102 | // Express uses basename on filename parameter | 107 | return res.download(path, downloadFilename) |
103 | const videoName = video.name.replace(/[/\\]/g, '_') | ||
104 | const filename = `${videoName}-${videoFile.resolution}p${videoFile.extname}` | ||
105 | |||
106 | return res.download(path, filename) | ||
107 | }) | 108 | }) |
108 | } | 109 | } |
109 | 110 | ||
@@ -136,14 +137,14 @@ async function downloadHLSVideoFile (req: express.Request, res: express.Response | |||
136 | 137 | ||
137 | if (!checkAllowResult(res, allowParameters, allowedResult)) return | 138 | if (!checkAllowResult(res, allowParameters, allowedResult)) return |
138 | 139 | ||
140 | const downloadFilename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}` | ||
141 | |||
139 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { | 142 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { |
140 | return redirectToObjectStorage({ req, res, video, file: videoFile }) | 143 | return redirectToObjectStorage({ req, res, video, streamingPlaylist, file: videoFile, downloadFilename }) |
141 | } | 144 | } |
142 | 145 | ||
143 | await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(streamingPlaylist), path => { | 146 | await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(streamingPlaylist), path => { |
144 | const filename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}` | 147 | return res.download(path, downloadFilename) |
145 | |||
146 | return res.download(path, filename) | ||
147 | }) | 148 | }) |
148 | } | 149 | } |
149 | 150 | ||
@@ -192,19 +193,21 @@ function checkAllowResult (res: express.Response, allowParameters: any, result?: | |||
192 | return true | 193 | return true |
193 | } | 194 | } |
194 | 195 | ||
195 | function redirectToObjectStorage (options: { | 196 | async function redirectToObjectStorage (options: { |
196 | req: express.Request | 197 | req: express.Request |
197 | res: express.Response | 198 | res: express.Response |
198 | video: MVideo | 199 | video: MVideo |
199 | file: MVideoFile | 200 | file: MVideoFile |
201 | streamingPlaylist?: MStreamingPlaylistVideo | ||
202 | downloadFilename: string | ||
200 | }) { | 203 | }) { |
201 | const { req, res, video, file } = options | 204 | const { res, video, streamingPlaylist, file, downloadFilename } = options |
202 | 205 | ||
203 | const baseUrl = file.getObjectStorageUrl(video) | 206 | const url = streamingPlaylist |
207 | ? await generateHLSFilePresignedUrl({ streamingPlaylist, file, downloadFilename }) | ||
208 | : await generateWebVideoPresignedUrl({ file, downloadFilename }) | ||
204 | 209 | ||
205 | const url = video.hasPrivateStaticPath() && req.query.videoFileToken | 210 | logger.debug('Generating pre-signed URL %s for video %s', url, video.uuid) |
206 | ? addQueryParams(baseUrl, { videoFileToken: req.query.videoFileToken }) | ||
207 | : baseUrl | ||
208 | 211 | ||
209 | return res.redirect(url) | 212 | return res.redirect(url) |
210 | } | 213 | } |
diff --git a/server/controllers/feeds/shared/video-feed-utils.ts b/server/controllers/feeds/shared/video-feed-utils.ts index 3175cea59..b154e04fa 100644 --- a/server/controllers/feeds/shared/video-feed-utils.ts +++ b/server/controllers/feeds/shared/video-feed-utils.ts | |||
@@ -2,7 +2,7 @@ import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown' | |||
2 | import { CONFIG } from '@server/initializers/config' | 2 | import { CONFIG } from '@server/initializers/config' |
3 | import { WEBSERVER } from '@server/initializers/constants' | 3 | import { WEBSERVER } from '@server/initializers/constants' |
4 | import { getServerActor } from '@server/models/application/application' | 4 | import { getServerActor } from '@server/models/application/application' |
5 | import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' | 5 | import { getCategoryLabel } from '@server/models/video/formatter' |
6 | import { DisplayOnlyForFollowerOptions } from '@server/models/video/sql/video' | 6 | import { DisplayOnlyForFollowerOptions } from '@server/models/video/sql/video' |
7 | import { VideoModel } from '@server/models/video/video' | 7 | import { VideoModel } from '@server/models/video/video' |
8 | import { MThumbnail, MUserDefault } from '@server/types/models' | 8 | import { MThumbnail, MUserDefault } from '@server/types/models' |
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts index b082e41f6..dad30365c 100644 --- a/server/controllers/lazy-static.ts +++ b/server/controllers/lazy-static.ts | |||
@@ -1,14 +1,28 @@ | |||
1 | import cors from 'cors' | 1 | import cors from 'cors' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' | 3 | import { CONFIG } from '@server/initializers/config' |
4 | import { MActorImage } from '@server/types/models' | ||
5 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' | 4 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' |
6 | import { logger } from '../helpers/logger' | 5 | import { FILES_CACHE, LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' |
7 | import { ACTOR_IMAGES_SIZE, LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' | 6 | import { |
8 | import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' | 7 | AvatarPermanentFileCache, |
9 | import { actorImagePathUnsafeCache, downloadActorImageFromWorker } from '../lib/local-actor' | 8 | VideoCaptionsSimpleFileCache, |
9 | VideoMiniaturePermanentFileCache, | ||
10 | VideoPreviewsSimpleFileCache, | ||
11 | VideoStoryboardsSimpleFileCache, | ||
12 | VideoTorrentsSimpleFileCache | ||
13 | } from '../lib/files-cache' | ||
10 | import { asyncMiddleware, handleStaticError } from '../middlewares' | 14 | import { asyncMiddleware, handleStaticError } from '../middlewares' |
11 | import { ActorImageModel } from '../models/actor/actor-image' | 15 | |
16 | // --------------------------------------------------------------------------- | ||
17 | // Cache initializations | ||
18 | // --------------------------------------------------------------------------- | ||
19 | |||
20 | VideoPreviewsSimpleFileCache.Instance.init(CONFIG.CACHE.PREVIEWS.SIZE, FILES_CACHE.PREVIEWS.MAX_AGE) | ||
21 | VideoCaptionsSimpleFileCache.Instance.init(CONFIG.CACHE.VIDEO_CAPTIONS.SIZE, FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE) | ||
22 | VideoTorrentsSimpleFileCache.Instance.init(CONFIG.CACHE.TORRENTS.SIZE, FILES_CACHE.TORRENTS.MAX_AGE) | ||
23 | VideoStoryboardsSimpleFileCache.Instance.init(CONFIG.CACHE.STORYBOARDS.SIZE, FILES_CACHE.STORYBOARDS.MAX_AGE) | ||
24 | |||
25 | // --------------------------------------------------------------------------- | ||
12 | 26 | ||
13 | const lazyStaticRouter = express.Router() | 27 | const lazyStaticRouter = express.Router() |
14 | 28 | ||
@@ -27,12 +41,24 @@ lazyStaticRouter.use( | |||
27 | ) | 41 | ) |
28 | 42 | ||
29 | lazyStaticRouter.use( | 43 | lazyStaticRouter.use( |
44 | LAZY_STATIC_PATHS.THUMBNAILS + ':filename', | ||
45 | asyncMiddleware(getThumbnail), | ||
46 | handleStaticError | ||
47 | ) | ||
48 | |||
49 | lazyStaticRouter.use( | ||
30 | LAZY_STATIC_PATHS.PREVIEWS + ':filename', | 50 | LAZY_STATIC_PATHS.PREVIEWS + ':filename', |
31 | asyncMiddleware(getPreview), | 51 | asyncMiddleware(getPreview), |
32 | handleStaticError | 52 | handleStaticError |
33 | ) | 53 | ) |
34 | 54 | ||
35 | lazyStaticRouter.use( | 55 | lazyStaticRouter.use( |
56 | LAZY_STATIC_PATHS.STORYBOARDS + ':filename', | ||
57 | asyncMiddleware(getStoryboard), | ||
58 | handleStaticError | ||
59 | ) | ||
60 | |||
61 | lazyStaticRouter.use( | ||
36 | LAZY_STATIC_PATHS.VIDEO_CAPTIONS + ':filename', | 62 | LAZY_STATIC_PATHS.VIDEO_CAPTIONS + ':filename', |
37 | asyncMiddleware(getVideoCaption), | 63 | asyncMiddleware(getVideoCaption), |
38 | handleStaticError | 64 | handleStaticError |
@@ -53,88 +79,48 @@ export { | |||
53 | } | 79 | } |
54 | 80 | ||
55 | // --------------------------------------------------------------------------- | 81 | // --------------------------------------------------------------------------- |
82 | const avatarPermanentFileCache = new AvatarPermanentFileCache() | ||
56 | 83 | ||
57 | async function getActorImage (req: express.Request, res: express.Response, next: express.NextFunction) { | 84 | function getActorImage (req: express.Request, res: express.Response, next: express.NextFunction) { |
58 | const filename = req.params.filename | 85 | const filename = req.params.filename |
59 | 86 | ||
60 | if (actorImagePathUnsafeCache.has(filename)) { | 87 | return avatarPermanentFileCache.lazyServe({ filename, res, next }) |
61 | return res.sendFile(actorImagePathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER }) | 88 | } |
62 | } | ||
63 | |||
64 | const image = await ActorImageModel.loadByName(filename) | ||
65 | if (!image) return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
66 | |||
67 | if (image.onDisk === false) { | ||
68 | if (!image.fileUrl) return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
69 | |||
70 | logger.info('Lazy serve remote actor image %s.', image.fileUrl) | ||
71 | |||
72 | try { | ||
73 | await downloadActorImageFromWorker({ | ||
74 | filename: image.filename, | ||
75 | fileUrl: image.fileUrl, | ||
76 | size: getActorImageSize(image), | ||
77 | type: image.type | ||
78 | }) | ||
79 | } catch (err) { | ||
80 | logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err }) | ||
81 | return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
82 | } | ||
83 | |||
84 | image.onDisk = true | ||
85 | image.save() | ||
86 | .catch(err => logger.error('Cannot save new actor image disk state.', { err })) | ||
87 | } | ||
88 | |||
89 | const path = image.getPath() | ||
90 | |||
91 | actorImagePathUnsafeCache.set(filename, path) | ||
92 | |||
93 | return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }, (err: any) => { | ||
94 | if (!err) return | ||
95 | |||
96 | // It seems this actor image is not on the disk anymore | ||
97 | if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) { | ||
98 | logger.error('Cannot lazy serve actor image %s.', filename, { err }) | ||
99 | 89 | ||
100 | actorImagePathUnsafeCache.delete(filename) | 90 | // --------------------------------------------------------------------------- |
91 | const videoMiniaturePermanentFileCache = new VideoMiniaturePermanentFileCache() | ||
101 | 92 | ||
102 | image.onDisk = false | 93 | function getThumbnail (req: express.Request, res: express.Response, next: express.NextFunction) { |
103 | image.save() | 94 | const filename = req.params.filename |
104 | .catch(err => logger.error('Cannot save new actor image disk state.', { err })) | ||
105 | } | ||
106 | 95 | ||
107 | return next(err) | 96 | return videoMiniaturePermanentFileCache.lazyServe({ filename, res, next }) |
108 | }) | ||
109 | } | 97 | } |
110 | 98 | ||
111 | function getActorImageSize (image: MActorImage): { width: number, height: number } { | 99 | // --------------------------------------------------------------------------- |
112 | if (image.width && image.height) { | 100 | |
113 | return { | 101 | async function getPreview (req: express.Request, res: express.Response) { |
114 | height: image.height, | 102 | const result = await VideoPreviewsSimpleFileCache.Instance.getFilePath(req.params.filename) |
115 | width: image.width | 103 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
116 | } | ||
117 | } | ||
118 | 104 | ||
119 | return ACTOR_IMAGES_SIZE[image.type][0] | 105 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) |
120 | } | 106 | } |
121 | 107 | ||
122 | async function getPreview (req: express.Request, res: express.Response) { | 108 | async function getStoryboard (req: express.Request, res: express.Response) { |
123 | const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename) | 109 | const result = await VideoStoryboardsSimpleFileCache.Instance.getFilePath(req.params.filename) |
124 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() | 110 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
125 | 111 | ||
126 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) | 112 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) |
127 | } | 113 | } |
128 | 114 | ||
129 | async function getVideoCaption (req: express.Request, res: express.Response) { | 115 | async function getVideoCaption (req: express.Request, res: express.Response) { |
130 | const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename) | 116 | const result = await VideoCaptionsSimpleFileCache.Instance.getFilePath(req.params.filename) |
131 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() | 117 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
132 | 118 | ||
133 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) | 119 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) |
134 | } | 120 | } |
135 | 121 | ||
136 | async function getTorrent (req: express.Request, res: express.Response) { | 122 | async function getTorrent (req: express.Request, res: express.Response) { |
137 | const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) | 123 | const result = await VideoTorrentsSimpleFileCache.Instance.getFilePath(req.params.filename) |
138 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() | 124 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
139 | 125 | ||
140 | // Torrents still use the old naming convention (video uuid + .torrent) | 126 | // Torrents still use the old naming convention (video uuid + .torrent) |
diff --git a/server/controllers/misc.ts b/server/controllers/misc.ts index 4c8af2adc..163352ac5 100644 --- a/server/controllers/misc.ts +++ b/server/controllers/misc.ts | |||
@@ -120,8 +120,8 @@ async function generateNodeinfo (req: express.Request, res: express.Response) { | |||
120 | hls: { | 120 | hls: { |
121 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | 121 | enabled: CONFIG.TRANSCODING.HLS.ENABLED |
122 | }, | 122 | }, |
123 | webtorrent: { | 123 | web_videos: { |
124 | enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED | 124 | enabled: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED |
125 | }, | 125 | }, |
126 | enabledResolutions: ServerConfigManager.Instance.getEnabledResolutions('vod') | 126 | enabledResolutions: ServerConfigManager.Instance.getEnabledResolutions('vod') |
127 | }, | 127 | }, |
diff --git a/server/controllers/object-storage-proxy.ts b/server/controllers/object-storage-proxy.ts index 8e2cc4af9..d0c59bf93 100644 --- a/server/controllers/object-storage-proxy.ts +++ b/server/controllers/object-storage-proxy.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import cors from 'cors' | 1 | import cors from 'cors' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | import { OBJECT_STORAGE_PROXY_PATHS } from '@server/initializers/constants' | 3 | import { OBJECT_STORAGE_PROXY_PATHS } from '@server/initializers/constants' |
4 | import { proxifyHLS, proxifyWebTorrentFile } from '@server/lib/object-storage' | 4 | import { proxifyHLS, proxifyWebVideoFile } from '@server/lib/object-storage' |
5 | import { | 5 | import { |
6 | asyncMiddleware, | 6 | asyncMiddleware, |
7 | ensureCanAccessPrivateVideoHLSFiles, | 7 | ensureCanAccessPrivateVideoHLSFiles, |
8 | ensureCanAccessVideoPrivateWebTorrentFiles, | 8 | ensureCanAccessVideoPrivateWebVideoFiles, |
9 | ensurePrivateObjectStorageProxyIsEnabled, | 9 | ensurePrivateObjectStorageProxyIsEnabled, |
10 | optionalAuthenticate | 10 | optionalAuthenticate |
11 | } from '@server/middlewares' | 11 | } from '@server/middlewares' |
@@ -15,11 +15,12 @@ const objectStorageProxyRouter = express.Router() | |||
15 | 15 | ||
16 | objectStorageProxyRouter.use(cors()) | 16 | objectStorageProxyRouter.use(cors()) |
17 | 17 | ||
18 | objectStorageProxyRouter.get(OBJECT_STORAGE_PROXY_PATHS.PRIVATE_WEBSEED + ':filename', | 18 | objectStorageProxyRouter.get( |
19 | [ OBJECT_STORAGE_PROXY_PATHS.PRIVATE_WEB_VIDEOS + ':filename', OBJECT_STORAGE_PROXY_PATHS.LEGACY_PRIVATE_WEB_VIDEOS + ':filename' ], | ||
19 | ensurePrivateObjectStorageProxyIsEnabled, | 20 | ensurePrivateObjectStorageProxyIsEnabled, |
20 | optionalAuthenticate, | 21 | optionalAuthenticate, |
21 | asyncMiddleware(ensureCanAccessVideoPrivateWebTorrentFiles), | 22 | asyncMiddleware(ensureCanAccessVideoPrivateWebVideoFiles), |
22 | asyncMiddleware(proxifyWebTorrentController) | 23 | asyncMiddleware(proxifyWebVideoController) |
23 | ) | 24 | ) |
24 | 25 | ||
25 | objectStorageProxyRouter.get(OBJECT_STORAGE_PROXY_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:filename', | 26 | objectStorageProxyRouter.get(OBJECT_STORAGE_PROXY_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:filename', |
@@ -35,10 +36,10 @@ export { | |||
35 | objectStorageProxyRouter | 36 | objectStorageProxyRouter |
36 | } | 37 | } |
37 | 38 | ||
38 | function proxifyWebTorrentController (req: express.Request, res: express.Response) { | 39 | function proxifyWebVideoController (req: express.Request, res: express.Response) { |
39 | const filename = req.params.filename | 40 | const filename = req.params.filename |
40 | 41 | ||
41 | return proxifyWebTorrentFile({ req, res, filename }) | 42 | return proxifyWebVideoFile({ req, res, filename }) |
42 | } | 43 | } |
43 | 44 | ||
44 | function proxifyHLSController (req: express.Request, res: express.Response) { | 45 | function proxifyHLSController (req: express.Request, res: express.Response) { |
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 9baff94c0..97caa8292 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -6,7 +6,7 @@ import { injectQueryToPlaylistUrls } from '@server/lib/hls' | |||
6 | import { | 6 | import { |
7 | asyncMiddleware, | 7 | asyncMiddleware, |
8 | ensureCanAccessPrivateVideoHLSFiles, | 8 | ensureCanAccessPrivateVideoHLSFiles, |
9 | ensureCanAccessVideoPrivateWebTorrentFiles, | 9 | ensureCanAccessVideoPrivateWebVideoFiles, |
10 | handleStaticError, | 10 | handleStaticError, |
11 | optionalAuthenticate | 11 | optionalAuthenticate |
12 | } from '@server/middlewares' | 12 | } from '@server/middlewares' |
@@ -21,21 +21,21 @@ const staticRouter = express.Router() | |||
21 | staticRouter.use(cors()) | 21 | staticRouter.use(cors()) |
22 | 22 | ||
23 | // --------------------------------------------------------------------------- | 23 | // --------------------------------------------------------------------------- |
24 | // WebTorrent/Classic videos | 24 | // Web videos/Classic videos |
25 | // --------------------------------------------------------------------------- | 25 | // --------------------------------------------------------------------------- |
26 | 26 | ||
27 | const privateWebTorrentStaticMiddlewares = CONFIG.STATIC_FILES.PRIVATE_FILES_REQUIRE_AUTH === true | 27 | const privateWebVideoStaticMiddlewares = CONFIG.STATIC_FILES.PRIVATE_FILES_REQUIRE_AUTH === true |
28 | ? [ optionalAuthenticate, asyncMiddleware(ensureCanAccessVideoPrivateWebTorrentFiles) ] | 28 | ? [ optionalAuthenticate, asyncMiddleware(ensureCanAccessVideoPrivateWebVideoFiles) ] |
29 | : [] | 29 | : [] |
30 | 30 | ||
31 | staticRouter.use( | 31 | staticRouter.use( |
32 | STATIC_PATHS.PRIVATE_WEBSEED, | 32 | [ STATIC_PATHS.PRIVATE_WEB_VIDEOS, STATIC_PATHS.LEGACY_PRIVATE_WEB_VIDEOS ], |
33 | ...privateWebTorrentStaticMiddlewares, | 33 | ...privateWebVideoStaticMiddlewares, |
34 | express.static(DIRECTORIES.VIDEOS.PRIVATE, { fallthrough: false }), | 34 | express.static(DIRECTORIES.VIDEOS.PRIVATE, { fallthrough: false }), |
35 | handleStaticError | 35 | handleStaticError |
36 | ) | 36 | ) |
37 | staticRouter.use( | 37 | staticRouter.use( |
38 | STATIC_PATHS.WEBSEED, | 38 | [ STATIC_PATHS.WEB_VIDEOS, STATIC_PATHS.LEGACY_WEB_VIDEOS ], |
39 | express.static(DIRECTORIES.VIDEOS.PUBLIC, { fallthrough: false }), | 39 | express.static(DIRECTORIES.VIDEOS.PUBLIC, { fallthrough: false }), |
40 | handleStaticError | 40 | handleStaticError |
41 | ) | 41 | ) |
@@ -72,7 +72,7 @@ staticRouter.use( | |||
72 | handleStaticError | 72 | handleStaticError |
73 | ) | 73 | ) |
74 | 74 | ||
75 | // Thumbnails path for express | 75 | // FIXME: deprecated in v6, to remove |
76 | const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR | 76 | const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR |
77 | staticRouter.use( | 77 | staticRouter.use( |
78 | STATIC_PATHS.THUMBNAILS, | 78 | STATIC_PATHS.THUMBNAILS, |
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts index 279ad83dc..7df47cf15 100644 --- a/server/helpers/custom-validators/activitypub/misc.ts +++ b/server/helpers/custom-validators/activitypub/misc.ts | |||
@@ -51,7 +51,8 @@ function setValidAttributedTo (obj: any) { | |||
51 | } | 51 | } |
52 | 52 | ||
53 | obj.attributedTo = obj.attributedTo.filter(a => { | 53 | obj.attributedTo = obj.attributedTo.filter(a => { |
54 | return (a.type === 'Group' || a.type === 'Person') && isActivityPubUrlValid(a.id) | 54 | return isActivityPubUrlValid(a) || |
55 | ((a.type === 'Group' || a.type === 'Person') && isActivityPubUrlValid(a.id)) | ||
55 | }) | 56 | }) |
56 | 57 | ||
57 | return true | 58 | return true |
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index 97b3577af..573a29754 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import validator from 'validator' | 1 | import validator from 'validator' |
2 | import { logger } from '@server/helpers/logger' | 2 | import { logger } from '@server/helpers/logger' |
3 | import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@shared/models' | 3 | import { ActivityPubStoryboard, ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject, VideoObject } from '@shared/models' |
4 | import { LiveVideoLatencyMode, VideoState } from '../../../../shared/models/videos' | 4 | import { LiveVideoLatencyMode, VideoState } from '../../../../shared/models/videos' |
5 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants' | 5 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants' |
6 | import { peertubeTruncate } from '../../core-utils' | 6 | import { peertubeTruncate } from '../../core-utils' |
@@ -48,6 +48,10 @@ function sanitizeAndCheckVideoTorrentObject (video: any) { | |||
48 | logger.debug('Video has invalid icons', { video }) | 48 | logger.debug('Video has invalid icons', { video }) |
49 | return false | 49 | return false |
50 | } | 50 | } |
51 | if (!setValidStoryboard(video)) { | ||
52 | logger.debug('Video has invalid preview (storyboard)', { video }) | ||
53 | return false | ||
54 | } | ||
51 | 55 | ||
52 | // Default attributes | 56 | // Default attributes |
53 | if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED | 57 | if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED |
@@ -201,3 +205,36 @@ function setRemoteVideoContent (video: any) { | |||
201 | 205 | ||
202 | return true | 206 | return true |
203 | } | 207 | } |
208 | |||
209 | function setValidStoryboard (video: VideoObject) { | ||
210 | if (!video.preview) return true | ||
211 | if (!Array.isArray(video.preview)) return false | ||
212 | |||
213 | video.preview = video.preview.filter(p => isStorybordValid(p)) | ||
214 | |||
215 | return true | ||
216 | } | ||
217 | |||
218 | function isStorybordValid (preview: ActivityPubStoryboard) { | ||
219 | if (!preview) return false | ||
220 | |||
221 | if ( | ||
222 | preview.type !== 'Image' || | ||
223 | !isArray(preview.rel) || | ||
224 | !preview.rel.includes('storyboard') | ||
225 | ) { | ||
226 | return false | ||
227 | } | ||
228 | |||
229 | preview.url = preview.url.filter(u => { | ||
230 | return u.mediaType === 'image/jpeg' && | ||
231 | isActivityPubUrlValid(u.href) && | ||
232 | validator.isInt(u.width + '', { min: 0 }) && | ||
233 | validator.isInt(u.height + '', { min: 0 }) && | ||
234 | validator.isInt(u.tileWidth + '', { min: 0 }) && | ||
235 | validator.isInt(u.tileHeight + '', { min: 0 }) && | ||
236 | isActivityPubVideoDurationValid(u.tileDuration) | ||
237 | }) | ||
238 | |||
239 | return preview.url.length !== 0 | ||
240 | } | ||
diff --git a/server/helpers/custom-validators/metrics.ts b/server/helpers/custom-validators/metrics.ts index 533f8988d..44a863630 100644 --- a/server/helpers/custom-validators/metrics.ts +++ b/server/helpers/custom-validators/metrics.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | function isValidPlayerMode (value: any) { | 1 | function isValidPlayerMode (value: any) { |
2 | return value === 'webtorrent' || value === 'p2p-media-loader' | 2 | // TODO: remove webtorrent in v7 |
3 | return value === 'webtorrent' || value === 'web-video' || value === 'p2p-media-loader' | ||
3 | } | 4 | } |
4 | 5 | ||
5 | // --------------------------------------------------------------------------- | 6 | // --------------------------------------------------------------------------- |
diff --git a/server/helpers/custom-validators/video-transcoding.ts b/server/helpers/custom-validators/video-transcoding.ts index cf792f996..220530de4 100644 --- a/server/helpers/custom-validators/video-transcoding.ts +++ b/server/helpers/custom-validators/video-transcoding.ts | |||
@@ -2,7 +2,7 @@ import { exists } from './misc' | |||
2 | 2 | ||
3 | function isValidCreateTranscodingType (value: any) { | 3 | function isValidCreateTranscodingType (value: any) { |
4 | return exists(value) && | 4 | return exists(value) && |
5 | (value === 'hls' || value === 'webtorrent') | 5 | (value === 'hls' || value === 'webtorrent' || value === 'web-video') // TODO: remove webtorrent in v7 |
6 | } | 6 | } |
7 | 7 | ||
8 | // --------------------------------------------------------------------------- | 8 | // --------------------------------------------------------------------------- |
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 5f75ec27c..91109217c 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { UploadFilesForCheck } from 'express' | 1 | import { Response, Request, UploadFilesForCheck } from 'express' |
2 | import { decode as magnetUriDecode } from 'magnet-uri' | 2 | import { decode as magnetUriDecode } from 'magnet-uri' |
3 | import validator from 'validator' | 3 | import validator from 'validator' |
4 | import { VideoFilter, VideoInclude, VideoPrivacy, VideoRateType } from '@shared/models' | 4 | import { HttpStatusCode, VideoFilter, VideoInclude, VideoPrivacy, VideoRateType } from '@shared/models' |
5 | import { | 5 | import { |
6 | CONSTRAINTS_FIELDS, | 6 | CONSTRAINTS_FIELDS, |
7 | MIMETYPES, | 7 | MIMETYPES, |
@@ -13,6 +13,7 @@ import { | |||
13 | VIDEO_STATES | 13 | VIDEO_STATES |
14 | } from '../../initializers/constants' | 14 | } from '../../initializers/constants' |
15 | import { exists, isArray, isDateValid, isFileValid } from './misc' | 15 | import { exists, isArray, isDateValid, isFileValid } from './misc' |
16 | import { getVideoWithAttributes } from '@server/helpers/video' | ||
16 | 17 | ||
17 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS | 18 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS |
18 | 19 | ||
@@ -110,6 +111,10 @@ function isVideoPrivacyValid (value: number) { | |||
110 | return VIDEO_PRIVACIES[value] !== undefined | 111 | return VIDEO_PRIVACIES[value] !== undefined |
111 | } | 112 | } |
112 | 113 | ||
114 | function isVideoReplayPrivacyValid (value: number) { | ||
115 | return VIDEO_PRIVACIES[value] !== undefined && value !== VideoPrivacy.PASSWORD_PROTECTED | ||
116 | } | ||
117 | |||
113 | function isScheduleVideoUpdatePrivacyValid (value: number) { | 118 | function isScheduleVideoUpdatePrivacyValid (value: number) { |
114 | return value === VideoPrivacy.UNLISTED || value === VideoPrivacy.PUBLIC || value === VideoPrivacy.INTERNAL | 119 | return value === VideoPrivacy.UNLISTED || value === VideoPrivacy.PUBLIC || value === VideoPrivacy.INTERNAL |
115 | } | 120 | } |
@@ -141,6 +146,49 @@ function isVideoMagnetUriValid (value: string) { | |||
141 | return parsed && isVideoFileInfoHashValid(parsed.infoHash) | 146 | return parsed && isVideoFileInfoHashValid(parsed.infoHash) |
142 | } | 147 | } |
143 | 148 | ||
149 | function isPasswordValid (password: string) { | ||
150 | return password.length >= CONSTRAINTS_FIELDS.VIDEO_PASSWORD.LENGTH.min && | ||
151 | password.length < CONSTRAINTS_FIELDS.VIDEO_PASSWORD.LENGTH.max | ||
152 | } | ||
153 | |||
154 | function isValidPasswordProtectedPrivacy (req: Request, res: Response) { | ||
155 | const fail = (message: string) => { | ||
156 | res.fail({ | ||
157 | status: HttpStatusCode.BAD_REQUEST_400, | ||
158 | message | ||
159 | }) | ||
160 | return false | ||
161 | } | ||
162 | |||
163 | let privacy: VideoPrivacy | ||
164 | const video = getVideoWithAttributes(res) | ||
165 | |||
166 | if (exists(req.body?.privacy)) privacy = req.body.privacy | ||
167 | else if (exists(video?.privacy)) privacy = video.privacy | ||
168 | |||
169 | if (privacy !== VideoPrivacy.PASSWORD_PROTECTED) return true | ||
170 | |||
171 | if (!exists(req.body.videoPasswords) && !exists(req.body.passwords)) return fail('Video passwords are missing.') | ||
172 | |||
173 | const passwords = req.body.videoPasswords || req.body.passwords | ||
174 | |||
175 | if (passwords.length === 0) return fail('At least one video password is required.') | ||
176 | |||
177 | if (new Set(passwords).size !== passwords.length) return fail('Duplicate video passwords are not allowed.') | ||
178 | |||
179 | for (const password of passwords) { | ||
180 | if (typeof password !== 'string') { | ||
181 | return fail('Video password should be a string.') | ||
182 | } | ||
183 | |||
184 | if (!isPasswordValid(password)) { | ||
185 | return fail('Invalid video password. Password length should be at least 2 characters and no more than 100 characters.') | ||
186 | } | ||
187 | } | ||
188 | |||
189 | return true | ||
190 | } | ||
191 | |||
144 | // --------------------------------------------------------------------------- | 192 | // --------------------------------------------------------------------------- |
145 | 193 | ||
146 | export { | 194 | export { |
@@ -164,9 +212,12 @@ export { | |||
164 | isVideoDurationValid, | 212 | isVideoDurationValid, |
165 | isVideoTagValid, | 213 | isVideoTagValid, |
166 | isVideoPrivacyValid, | 214 | isVideoPrivacyValid, |
215 | isVideoReplayPrivacyValid, | ||
167 | isVideoFileResolutionValid, | 216 | isVideoFileResolutionValid, |
168 | isVideoFileSizeValid, | 217 | isVideoFileSizeValid, |
169 | isVideoImageValid, | 218 | isVideoImageValid, |
170 | isVideoSupportValid, | 219 | isVideoSupportValid, |
171 | isVideoFilterValid | 220 | isVideoFilterValid, |
221 | isPasswordValid, | ||
222 | isValidPasswordProtectedPrivacy | ||
172 | } | 223 | } |
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index 82dd4c178..783097e55 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import express, { RequestHandler } from 'express' | 1 | import express, { RequestHandler } from 'express' |
2 | import multer, { diskStorage } from 'multer' | 2 | import multer, { diskStorage } from 'multer' |
3 | import { getLowercaseExtension } from '@shared/core-utils' | 3 | import { getLowercaseExtension } from '@shared/core-utils' |
4 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' | ||
5 | import { CONFIG } from '../initializers/config' | 4 | import { CONFIG } from '../initializers/config' |
6 | import { REMOTE_SCHEME } from '../initializers/constants' | 5 | import { REMOTE_SCHEME } from '../initializers/constants' |
7 | import { isArray } from './custom-validators/misc' | 6 | import { isArray } from './custom-validators/misc' |
@@ -59,12 +58,6 @@ function getHostWithPort (host: string) { | |||
59 | return host | 58 | return host |
60 | } | 59 | } |
61 | 60 | ||
62 | function badRequest (_req: express.Request, res: express.Response) { | ||
63 | return res.type('json') | ||
64 | .status(HttpStatusCode.BAD_REQUEST_400) | ||
65 | .end() | ||
66 | } | ||
67 | |||
68 | function createReqFiles ( | 61 | function createReqFiles ( |
69 | fieldNames: string[], | 62 | fieldNames: string[], |
70 | mimeTypes: { [id: string]: string | string[] }, | 63 | mimeTypes: { [id: string]: string | string[] }, |
@@ -126,7 +119,6 @@ export { | |||
126 | getHostWithPort, | 119 | getHostWithPort, |
127 | createAnyReqFiles, | 120 | createAnyReqFiles, |
128 | isUserAbleToSearchRemoteURI, | 121 | isUserAbleToSearchRemoteURI, |
129 | badRequest, | ||
130 | createReqFiles, | 122 | createReqFiles, |
131 | cleanUpReqFiles, | 123 | cleanUpReqFiles, |
132 | getCountVideos | 124 | getCountVideos |
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts index f86f7216d..7b77e694a 100644 --- a/server/helpers/image-utils.ts +++ b/server/helpers/image-utils.ts | |||
@@ -51,7 +51,7 @@ async function generateImageFromVideoFile (options: { | |||
51 | const pendingImagePath = join(folder, pendingImageName) | 51 | const pendingImagePath = join(folder, pendingImageName) |
52 | 52 | ||
53 | try { | 53 | try { |
54 | await generateThumbnailFromVideo({ fromPath, folder, imageName }) | 54 | await generateThumbnailFromVideo({ fromPath, output: pendingImagePath }) |
55 | 55 | ||
56 | const destination = join(folder, imageName) | 56 | const destination = join(folder, imageName) |
57 | await processImage({ path: pendingImagePath, destination, newSize: size }) | 57 | await processImage({ path: pendingImagePath, destination, newSize: size }) |
diff --git a/server/helpers/promise-cache.ts b/server/helpers/promise-cache.ts index 07e8a9962..303bab976 100644 --- a/server/helpers/promise-cache.ts +++ b/server/helpers/promise-cache.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | export class PromiseCache <A, R> { | 1 | export class CachePromiseFactory <A, R> { |
2 | private readonly running = new Map<string, Promise<R>>() | 2 | private readonly running = new Map<string, Promise<R>>() |
3 | 3 | ||
4 | constructor ( | 4 | constructor ( |
@@ -8,14 +8,32 @@ export class PromiseCache <A, R> { | |||
8 | } | 8 | } |
9 | 9 | ||
10 | run (arg: A) { | 10 | run (arg: A) { |
11 | return this.runWithContext(null, arg) | ||
12 | } | ||
13 | |||
14 | runWithContext (ctx: any, arg: A) { | ||
11 | const key = this.keyBuilder(arg) | 15 | const key = this.keyBuilder(arg) |
12 | 16 | ||
13 | if (this.running.has(key)) return this.running.get(key) | 17 | if (this.running.has(key)) return this.running.get(key) |
14 | 18 | ||
15 | const p = this.fn(arg) | 19 | const p = this.fn.apply(ctx || this, [ arg ]) |
16 | 20 | ||
17 | this.running.set(key, p) | 21 | this.running.set(key, p) |
18 | 22 | ||
19 | return p.finally(() => this.running.delete(key)) | 23 | return p.finally(() => this.running.delete(key)) |
20 | } | 24 | } |
21 | } | 25 | } |
26 | |||
27 | export function CachePromise (options: { | ||
28 | keyBuilder: (...args: any[]) => string | ||
29 | }) { | ||
30 | return function (_target, _key, descriptor: PropertyDescriptor) { | ||
31 | const promiseCache = new CachePromiseFactory(descriptor.value, options.keyBuilder) | ||
32 | |||
33 | descriptor.value = function () { | ||
34 | if (arguments.length !== 1) throw new Error('Cache promise only support methods with 1 argument') | ||
35 | |||
36 | return promiseCache.runWithContext(this, arguments[0]) | ||
37 | } | ||
38 | } | ||
39 | } | ||
diff --git a/server/helpers/query.ts b/server/helpers/query.ts index 10efae41c..c0f78368f 100644 --- a/server/helpers/query.ts +++ b/server/helpers/query.ts | |||
@@ -23,7 +23,8 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) { | |||
23 | 'include', | 23 | 'include', |
24 | 'skipCount', | 24 | 'skipCount', |
25 | 'hasHLSFiles', | 25 | 'hasHLSFiles', |
26 | 'hasWebtorrentFiles', | 26 | 'hasWebtorrentFiles', // TODO: Remove in v7 |
27 | 'hasWebVideoFiles', | ||
27 | 'search', | 28 | 'search', |
28 | 'excludeAlreadyWatched' | 29 | 'excludeAlreadyWatched' |
29 | ]) | 30 | ]) |
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts index 68dea909d..5ef72058b 100644 --- a/server/initializers/checker-after-init.ts +++ b/server/initializers/checker-after-init.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import config from 'config' | 1 | import config from 'config' |
2 | import { readFileSync, writeFileSync } from 'fs-extra' | ||
2 | import { URL } from 'url' | 3 | import { URL } from 'url' |
3 | import { uniqify } from '@shared/core-utils' | 4 | import { uniqify } from '@shared/core-utils' |
4 | import { getFFmpegVersion } from '@shared/ffmpeg' | 5 | import { getFFmpegVersion } from '@shared/ffmpeg' |
@@ -10,7 +11,7 @@ import { logger } from '../helpers/logger' | |||
10 | import { ApplicationModel, getServerActor } from '../models/application/application' | 11 | import { ApplicationModel, getServerActor } from '../models/application/application' |
11 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 12 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
12 | import { UserModel } from '../models/user/user' | 13 | import { UserModel } from '../models/user/user' |
13 | import { CONFIG, isEmailEnabled } from './config' | 14 | import { CONFIG, getLocalConfigFilePath, isEmailEnabled, reloadConfig } from './config' |
14 | import { WEBSERVER } from './constants' | 15 | import { WEBSERVER } from './constants' |
15 | 16 | ||
16 | async function checkActivityPubUrls () { | 17 | async function checkActivityPubUrls () { |
@@ -37,10 +38,7 @@ function checkConfig () { | |||
37 | const configFiles = config.util.getConfigSources().map(s => s.name).join(' -> ') | 38 | const configFiles = config.util.getConfigSources().map(s => s.name).join(' -> ') |
38 | logger.info('Using following configuration file hierarchy: %s.', configFiles) | 39 | logger.info('Using following configuration file hierarchy: %s.', configFiles) |
39 | 40 | ||
40 | // Moved configuration keys | 41 | checkRemovedConfigKeys() |
41 | if (config.has('services.csp-logger')) { | ||
42 | logger.warn('services.csp-logger configuration has been renamed to csp.report_uri. Please update your configuration file.') | ||
43 | } | ||
44 | 42 | ||
45 | checkSecretsConfig() | 43 | checkSecretsConfig() |
46 | checkEmailConfig() | 44 | checkEmailConfig() |
@@ -104,6 +102,34 @@ export { | |||
104 | 102 | ||
105 | // --------------------------------------------------------------------------- | 103 | // --------------------------------------------------------------------------- |
106 | 104 | ||
105 | function checkRemovedConfigKeys () { | ||
106 | // Moved configuration keys | ||
107 | if (config.has('services.csp-logger')) { | ||
108 | logger.warn('services.csp-logger configuration has been renamed to csp.report_uri. Please update your configuration file.') | ||
109 | } | ||
110 | |||
111 | if (config.has('transcoding.webtorrent.enabled')) { | ||
112 | const localConfigPath = getLocalConfigFilePath() | ||
113 | |||
114 | const content = readFileSync(localConfigPath, { encoding: 'utf-8' }) | ||
115 | if (!content.includes('"webtorrent"')) { | ||
116 | throw new Error('Please rename transcoding.webtorrent.enabled key to transcoding.web_videos.enabled in your configuration file') | ||
117 | } | ||
118 | |||
119 | try { | ||
120 | logger.info( | ||
121 | 'Replacing "transcoding.webtorrent.enabled" key to "transcoding.web_videos.enabled" in your local configuration ' + localConfigPath | ||
122 | ) | ||
123 | |||
124 | writeFileSync(localConfigPath, content.replace('"webtorrent"', '"web_videos"'), { encoding: 'utf-8' }) | ||
125 | |||
126 | reloadConfig() | ||
127 | } catch (err) { | ||
128 | logger.error('Cannot write new configuration to file ' + localConfigPath, { err }) | ||
129 | } | ||
130 | } | ||
131 | } | ||
132 | |||
107 | function checkSecretsConfig () { | 133 | function checkSecretsConfig () { |
108 | if (!CONFIG.SECRETS.PEERTUBE) { | 134 | if (!CONFIG.SECRETS.PEERTUBE) { |
109 | throw new Error('secrets.peertube is missing in config. Generate one using `openssl rand -hex 32`') | 135 | throw new Error('secrets.peertube is missing in config. Generate one using `openssl rand -hex 32`') |
@@ -191,15 +217,15 @@ function checkStorageConfig () { | |||
191 | } | 217 | } |
192 | } | 218 | } |
193 | 219 | ||
194 | if (CONFIG.STORAGE.VIDEOS_DIR === CONFIG.STORAGE.REDUNDANCY_DIR) { | 220 | if (CONFIG.STORAGE.WEB_VIDEOS_DIR === CONFIG.STORAGE.REDUNDANCY_DIR) { |
195 | logger.warn('Redundancy directory should be different than the videos folder.') | 221 | logger.warn('Redundancy directory should be different than the videos folder.') |
196 | } | 222 | } |
197 | } | 223 | } |
198 | 224 | ||
199 | function checkTranscodingConfig () { | 225 | function checkTranscodingConfig () { |
200 | if (CONFIG.TRANSCODING.ENABLED) { | 226 | if (CONFIG.TRANSCODING.ENABLED) { |
201 | if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false && CONFIG.TRANSCODING.HLS.ENABLED === false) { | 227 | if (CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED === false && CONFIG.TRANSCODING.HLS.ENABLED === false) { |
202 | throw new Error('You need to enable at least WebTorrent transcoding or HLS transcoding.') | 228 | throw new Error('You need to enable at least Web Video transcoding or HLS transcoding.') |
203 | } | 229 | } |
204 | 230 | ||
205 | if (CONFIG.TRANSCODING.CONCURRENCY <= 0) { | 231 | if (CONFIG.TRANSCODING.CONCURRENCY <= 0) { |
@@ -264,7 +290,7 @@ function checkLiveConfig () { | |||
264 | function checkObjectStorageConfig () { | 290 | function checkObjectStorageConfig () { |
265 | if (CONFIG.OBJECT_STORAGE.ENABLED === true) { | 291 | if (CONFIG.OBJECT_STORAGE.ENABLED === true) { |
266 | 292 | ||
267 | if (!CONFIG.OBJECT_STORAGE.VIDEOS.BUCKET_NAME) { | 293 | if (!CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BUCKET_NAME) { |
268 | throw new Error('videos_bucket should be set when object storage support is enabled.') | 294 | throw new Error('videos_bucket should be set when object storage support is enabled.') |
269 | } | 295 | } |
270 | 296 | ||
@@ -273,10 +299,10 @@ function checkObjectStorageConfig () { | |||
273 | } | 299 | } |
274 | 300 | ||
275 | if ( | 301 | if ( |
276 | CONFIG.OBJECT_STORAGE.VIDEOS.BUCKET_NAME === CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET_NAME && | 302 | CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BUCKET_NAME === CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET_NAME && |
277 | CONFIG.OBJECT_STORAGE.VIDEOS.PREFIX === CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.PREFIX | 303 | CONFIG.OBJECT_STORAGE.WEB_VIDEOS.PREFIX === CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.PREFIX |
278 | ) { | 304 | ) { |
279 | if (CONFIG.OBJECT_STORAGE.VIDEOS.PREFIX === '') { | 305 | if (CONFIG.OBJECT_STORAGE.WEB_VIDEOS.PREFIX === '') { |
280 | throw new Error('Object storage bucket prefixes should be set when the same bucket is used for both types of video.') | 306 | throw new Error('Object storage bucket prefixes should be set when the same bucket is used for both types of video.') |
281 | } | 307 | } |
282 | 308 | ||
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 0a315ea70..a872fcba3 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts | |||
@@ -18,7 +18,7 @@ function checkMissedConfig () { | |||
18 | 'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max', | 18 | 'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max', |
19 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', | 19 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', |
20 | 'email.body.signature', 'email.subject.prefix', | 20 | 'email.body.signature', 'email.subject.prefix', |
21 | 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', | 21 | 'storage.avatars', 'storage.web_videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', |
22 | 'storage.redundancy', 'storage.tmp', 'storage.streaming_playlists', 'storage.plugins', 'storage.well_known', | 22 | 'storage.redundancy', 'storage.tmp', 'storage.streaming_playlists', 'storage.plugins', 'storage.well_known', |
23 | 'log.level', 'log.rotation.enabled', 'log.rotation.max_file_size', 'log.rotation.max_files', 'log.anonymize_ip', | 23 | 'log.level', 'log.rotation.enabled', 'log.rotation.max_file_size', 'log.rotation.max_files', 'log.anonymize_ip', |
24 | 'log.log_ping_requests', 'log.log_tracker_unknown_infohash', 'log.prettify_sql', 'log.accept_client_log', | 24 | 'log.log_ping_requests', 'log.log_tracker_unknown_infohash', 'log.prettify_sql', 'log.accept_client_log', |
@@ -29,12 +29,13 @@ function checkMissedConfig () { | |||
29 | 'video_channels.max_per_user', | 29 | 'video_channels.max_per_user', |
30 | 'csp.enabled', 'csp.report_only', 'csp.report_uri', | 30 | 'csp.enabled', 'csp.report_only', 'csp.report_uri', |
31 | 'security.frameguard.enabled', 'security.powered_by_header.enabled', | 31 | 'security.frameguard.enabled', 'security.powered_by_header.enabled', |
32 | 'cache.previews.size', 'cache.captions.size', 'cache.torrents.size', 'admin.email', 'contact_form.enabled', | 32 | 'cache.previews.size', 'cache.captions.size', 'cache.torrents.size', 'cache.storyboards.size', |
33 | 'admin.email', 'contact_form.enabled', | ||
33 | 'signup.enabled', 'signup.limit', 'signup.requires_approval', 'signup.requires_email_verification', 'signup.minimum_age', | 34 | 'signup.enabled', 'signup.limit', 'signup.requires_approval', 'signup.requires_email_verification', 'signup.minimum_age', |
34 | 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', | 35 | 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', |
35 | 'redundancy.videos.strategies', 'redundancy.videos.check_interval', | 36 | 'redundancy.videos.strategies', 'redundancy.videos.check_interval', |
36 | 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.hls.enabled', | 37 | 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.web_videos.enabled', |
37 | 'transcoding.profile', 'transcoding.concurrency', | 38 | 'transcoding.hls.enabled', 'transcoding.profile', 'transcoding.concurrency', |
38 | 'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p', | 39 | 'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p', |
39 | 'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p', | 40 | 'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p', |
40 | 'transcoding.resolutions.2160p', 'transcoding.always_transcode_original_resolution', 'transcoding.remote_runners.enabled', | 41 | 'transcoding.resolutions.2160p', 'transcoding.always_transcode_original_resolution', 'transcoding.remote_runners.enabled', |
@@ -59,8 +60,8 @@ function checkMissedConfig () { | |||
59 | 'object_storage.enabled', 'object_storage.endpoint', 'object_storage.region', 'object_storage.upload_acl.public', | 60 | 'object_storage.enabled', 'object_storage.endpoint', 'object_storage.region', 'object_storage.upload_acl.public', |
60 | 'object_storage.upload_acl.private', 'object_storage.proxy.proxify_private_files', 'object_storage.credentials.access_key_id', | 61 | 'object_storage.upload_acl.private', 'object_storage.proxy.proxify_private_files', 'object_storage.credentials.access_key_id', |
61 | 'object_storage.credentials.secret_access_key', 'object_storage.max_upload_part', 'object_storage.streaming_playlists.bucket_name', | 62 | 'object_storage.credentials.secret_access_key', 'object_storage.max_upload_part', 'object_storage.streaming_playlists.bucket_name', |
62 | 'object_storage.streaming_playlists.prefix', 'object_storage.streaming_playlists.base_url', 'object_storage.videos.bucket_name', | 63 | 'object_storage.streaming_playlists.prefix', 'object_storage.streaming_playlists.base_url', 'object_storage.web_videos.bucket_name', |
63 | 'object_storage.videos.prefix', 'object_storage.videos.base_url', | 64 | 'object_storage.web_videos.prefix', 'object_storage.web_videos.base_url', |
64 | 'theme.default', | 65 | 'theme.default', |
65 | 'feeds.videos.count', 'feeds.comments.count', | 66 | 'feeds.videos.count', 'feeds.comments.count', |
66 | 'geo_ip.enabled', 'geo_ip.country.database_url', | 67 | 'geo_ip.enabled', 'geo_ip.country.database_url', |
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 51ac5d0ce..37cd852f1 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -106,12 +106,13 @@ const CONFIG = { | |||
106 | TMP_DIR: buildPath(config.get<string>('storage.tmp')), | 106 | TMP_DIR: buildPath(config.get<string>('storage.tmp')), |
107 | TMP_PERSISTENT_DIR: buildPath(config.get<string>('storage.tmp_persistent')), | 107 | TMP_PERSISTENT_DIR: buildPath(config.get<string>('storage.tmp_persistent')), |
108 | BIN_DIR: buildPath(config.get<string>('storage.bin')), | 108 | BIN_DIR: buildPath(config.get<string>('storage.bin')), |
109 | ACTOR_IMAGES: buildPath(config.get<string>('storage.avatars')), | 109 | ACTOR_IMAGES_DIR: buildPath(config.get<string>('storage.avatars')), |
110 | LOG_DIR: buildPath(config.get<string>('storage.logs')), | 110 | LOG_DIR: buildPath(config.get<string>('storage.logs')), |
111 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), | 111 | WEB_VIDEOS_DIR: buildPath(config.get<string>('storage.web_videos')), |
112 | STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')), | 112 | STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')), |
113 | REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')), | 113 | REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')), |
114 | THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), | 114 | THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), |
115 | STORYBOARDS_DIR: buildPath(config.get<string>('storage.storyboards')), | ||
115 | PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), | 116 | PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), |
116 | CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')), | 117 | CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')), |
117 | TORRENTS_DIR: buildPath(config.get<string>('storage.torrents')), | 118 | TORRENTS_DIR: buildPath(config.get<string>('storage.torrents')), |
@@ -139,10 +140,10 @@ const CONFIG = { | |||
139 | PROXY: { | 140 | PROXY: { |
140 | PROXIFY_PRIVATE_FILES: config.get<boolean>('object_storage.proxy.proxify_private_files') | 141 | PROXIFY_PRIVATE_FILES: config.get<boolean>('object_storage.proxy.proxify_private_files') |
141 | }, | 142 | }, |
142 | VIDEOS: { | 143 | WEB_VIDEOS: { |
143 | BUCKET_NAME: config.get<string>('object_storage.videos.bucket_name'), | 144 | BUCKET_NAME: config.get<string>('object_storage.web_videos.bucket_name'), |
144 | PREFIX: config.get<string>('object_storage.videos.prefix'), | 145 | PREFIX: config.get<string>('object_storage.web_videos.prefix'), |
145 | BASE_URL: config.get<string>('object_storage.videos.base_url') | 146 | BASE_URL: config.get<string>('object_storage.web_videos.base_url') |
146 | }, | 147 | }, |
147 | STREAMING_PLAYLISTS: { | 148 | STREAMING_PLAYLISTS: { |
148 | BUCKET_NAME: config.get<string>('object_storage.streaming_playlists.bucket_name'), | 149 | BUCKET_NAME: config.get<string>('object_storage.streaming_playlists.bucket_name'), |
@@ -370,8 +371,8 @@ const CONFIG = { | |||
370 | HLS: { | 371 | HLS: { |
371 | get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') } | 372 | get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') } |
372 | }, | 373 | }, |
373 | WEBTORRENT: { | 374 | WEB_VIDEOS: { |
374 | get ENABLED () { return config.get<boolean>('transcoding.webtorrent.enabled') } | 375 | get ENABLED () { return config.get<boolean>('transcoding.web_videos.enabled') } |
375 | }, | 376 | }, |
376 | REMOTE_RUNNERS: { | 377 | REMOTE_RUNNERS: { |
377 | get ENABLED () { return config.get<boolean>('transcoding.remote_runners.enabled') } | 378 | get ENABLED () { return config.get<boolean>('transcoding.remote_runners.enabled') } |
@@ -482,6 +483,9 @@ const CONFIG = { | |||
482 | }, | 483 | }, |
483 | TORRENTS: { | 484 | TORRENTS: { |
484 | get SIZE () { return config.get<number>('cache.torrents.size') } | 485 | get SIZE () { return config.get<number>('cache.torrents.size') } |
486 | }, | ||
487 | STORYBOARDS: { | ||
488 | get SIZE () { return config.get<number>('cache.storyboards.size') } | ||
485 | } | 489 | } |
486 | }, | 490 | }, |
487 | INSTANCE: { | 491 | INSTANCE: { |
@@ -580,16 +584,6 @@ function isEmailEnabled () { | |||
580 | return false | 584 | return false |
581 | } | 585 | } |
582 | 586 | ||
583 | // --------------------------------------------------------------------------- | ||
584 | |||
585 | export { | ||
586 | CONFIG, | ||
587 | registerConfigChangedHandler, | ||
588 | isEmailEnabled | ||
589 | } | ||
590 | |||
591 | // --------------------------------------------------------------------------- | ||
592 | |||
593 | function getLocalConfigFilePath () { | 587 | function getLocalConfigFilePath () { |
594 | const localConfigDir = getLocalConfigDir() | 588 | const localConfigDir = getLocalConfigDir() |
595 | 589 | ||
@@ -600,6 +594,17 @@ function getLocalConfigFilePath () { | |||
600 | return join(localConfigDir, filename + '.json') | 594 | return join(localConfigDir, filename + '.json') |
601 | } | 595 | } |
602 | 596 | ||
597 | // --------------------------------------------------------------------------- | ||
598 | |||
599 | export { | ||
600 | CONFIG, | ||
601 | getLocalConfigFilePath, | ||
602 | registerConfigChangedHandler, | ||
603 | isEmailEnabled | ||
604 | } | ||
605 | |||
606 | // --------------------------------------------------------------------------- | ||
607 | |||
603 | function getLocalConfigDir () { | 608 | function getLocalConfigDir () { |
604 | if (process.env.PEERTUBE_LOCAL_CONFIG) return process.env.PEERTUBE_LOCAL_CONFIG | 609 | if (process.env.PEERTUBE_LOCAL_CONFIG) return process.env.PEERTUBE_LOCAL_CONFIG |
605 | 610 | ||
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index a92fd22d6..03ae94d35 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -27,7 +27,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
27 | 27 | ||
28 | // --------------------------------------------------------------------------- | 28 | // --------------------------------------------------------------------------- |
29 | 29 | ||
30 | const LAST_MIGRATION_VERSION = 780 | 30 | const LAST_MIGRATION_VERSION = 790 |
31 | 31 | ||
32 | // --------------------------------------------------------------------------- | 32 | // --------------------------------------------------------------------------- |
33 | 33 | ||
@@ -76,6 +76,8 @@ const SORTABLE_COLUMNS = { | |||
76 | VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ], | 76 | VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ], |
77 | VIDEO_COMMENTS: [ 'createdAt' ], | 77 | VIDEO_COMMENTS: [ 'createdAt' ], |
78 | 78 | ||
79 | VIDEO_PASSWORDS: [ 'createdAt' ], | ||
80 | |||
79 | VIDEO_RATES: [ 'createdAt' ], | 81 | VIDEO_RATES: [ 'createdAt' ], |
80 | BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ], | 82 | BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ], |
81 | 83 | ||
@@ -172,6 +174,7 @@ const JOB_ATTEMPTS: { [id in JobType]: number } = { | |||
172 | 'after-video-channel-import': 1, | 174 | 'after-video-channel-import': 1, |
173 | 'move-to-object-storage': 3, | 175 | 'move-to-object-storage': 3, |
174 | 'transcoding-job-builder': 1, | 176 | 'transcoding-job-builder': 1, |
177 | 'generate-video-storyboard': 1, | ||
175 | 'notify': 1, | 178 | 'notify': 1, |
176 | 'federate-video': 1 | 179 | 'federate-video': 1 |
177 | } | 180 | } |
@@ -196,6 +199,7 @@ const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-im | |||
196 | 'video-channel-import': 1, | 199 | 'video-channel-import': 1, |
197 | 'after-video-channel-import': 1, | 200 | 'after-video-channel-import': 1, |
198 | 'transcoding-job-builder': 1, | 201 | 'transcoding-job-builder': 1, |
202 | 'generate-video-storyboard': 1, | ||
199 | 'notify': 5, | 203 | 'notify': 5, |
200 | 'federate-video': 3 | 204 | 'federate-video': 3 |
201 | } | 205 | } |
@@ -216,6 +220,7 @@ const JOB_TTL: { [id in JobType]: number } = { | |||
216 | 'activitypub-refresher': 60000 * 10, // 10 minutes | 220 | 'activitypub-refresher': 60000 * 10, // 10 minutes |
217 | 'video-redundancy': 1000 * 3600 * 3, // 3 hours | 221 | 'video-redundancy': 1000 * 3600 * 3, // 3 hours |
218 | 'video-live-ending': 1000 * 60 * 10, // 10 minutes | 222 | 'video-live-ending': 1000 * 60 * 10, // 10 minutes |
223 | 'generate-video-storyboard': 1000 * 60 * 10, // 10 minutes | ||
219 | 'manage-video-torrent': 1000 * 3600 * 3, // 3 hours | 224 | 'manage-video-torrent': 1000 * 3600 * 3, // 3 hours |
220 | 'move-to-object-storage': 1000 * 60 * 60 * 3, // 3 hours | 225 | 'move-to-object-storage': 1000 * 60 * 60 * 3, // 3 hours |
221 | 'video-channel-import': 1000 * 60 * 60 * 4, // 4 hours | 226 | 'video-channel-import': 1000 * 60 * 60 * 4, // 4 hours |
@@ -444,6 +449,9 @@ const CONSTRAINTS_FIELDS = { | |||
444 | REASON: { min: 1, max: 5000 }, // Length | 449 | REASON: { min: 1, max: 5000 }, // Length |
445 | ERROR_MESSAGE: { min: 1, max: 5000 }, // Length | 450 | ERROR_MESSAGE: { min: 1, max: 5000 }, // Length |
446 | PROGRESS: { min: 0, max: 100 } // Value | 451 | PROGRESS: { min: 0, max: 100 } // Value |
452 | }, | ||
453 | VIDEO_PASSWORD: { | ||
454 | LENGTH: { min: 2, max: 100 } | ||
447 | } | 455 | } |
448 | } | 456 | } |
449 | 457 | ||
@@ -520,7 +528,8 @@ const VIDEO_PRIVACIES: { [ id in VideoPrivacy ]: string } = { | |||
520 | [VideoPrivacy.PUBLIC]: 'Public', | 528 | [VideoPrivacy.PUBLIC]: 'Public', |
521 | [VideoPrivacy.UNLISTED]: 'Unlisted', | 529 | [VideoPrivacy.UNLISTED]: 'Unlisted', |
522 | [VideoPrivacy.PRIVATE]: 'Private', | 530 | [VideoPrivacy.PRIVATE]: 'Private', |
523 | [VideoPrivacy.INTERNAL]: 'Internal' | 531 | [VideoPrivacy.INTERNAL]: 'Internal', |
532 | [VideoPrivacy.PASSWORD_PROTECTED]: 'Password protected' | ||
524 | } | 533 | } |
525 | 534 | ||
526 | const VIDEO_STATES: { [ id in VideoState ]: string } = { | 535 | const VIDEO_STATES: { [ id in VideoState ]: string } = { |
@@ -738,10 +747,16 @@ const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = { | |||
738 | 747 | ||
739 | // Express static paths (router) | 748 | // Express static paths (router) |
740 | const STATIC_PATHS = { | 749 | const STATIC_PATHS = { |
750 | // TODO: deprecated in v6, to remove | ||
741 | THUMBNAILS: '/static/thumbnails/', | 751 | THUMBNAILS: '/static/thumbnails/', |
742 | 752 | ||
743 | WEBSEED: '/static/webseed/', | 753 | // Need to keep this legacy path for previously generated torrents |
744 | PRIVATE_WEBSEED: '/static/webseed/private/', | 754 | LEGACY_WEB_VIDEOS: '/static/webseed/', |
755 | WEB_VIDEOS: '/static/web-videos/', | ||
756 | |||
757 | // Need to keep this legacy path for previously generated torrents | ||
758 | LEGACY_PRIVATE_WEB_VIDEOS: '/static/webseed/private/', | ||
759 | PRIVATE_WEB_VIDEOS: '/static/web-videos/private/', | ||
745 | 760 | ||
746 | REDUNDANCY: '/static/redundancy/', | 761 | REDUNDANCY: '/static/redundancy/', |
747 | 762 | ||
@@ -756,14 +771,18 @@ const STATIC_DOWNLOAD_PATHS = { | |||
756 | HLS_VIDEOS: '/download/streaming-playlists/hls/videos/' | 771 | HLS_VIDEOS: '/download/streaming-playlists/hls/videos/' |
757 | } | 772 | } |
758 | const LAZY_STATIC_PATHS = { | 773 | const LAZY_STATIC_PATHS = { |
774 | THUMBNAILS: '/lazy-static/thumbnails/', | ||
759 | BANNERS: '/lazy-static/banners/', | 775 | BANNERS: '/lazy-static/banners/', |
760 | AVATARS: '/lazy-static/avatars/', | 776 | AVATARS: '/lazy-static/avatars/', |
761 | PREVIEWS: '/lazy-static/previews/', | 777 | PREVIEWS: '/lazy-static/previews/', |
762 | VIDEO_CAPTIONS: '/lazy-static/video-captions/', | 778 | VIDEO_CAPTIONS: '/lazy-static/video-captions/', |
763 | TORRENTS: '/lazy-static/torrents/' | 779 | TORRENTS: '/lazy-static/torrents/', |
780 | STORYBOARDS: '/lazy-static/storyboards/' | ||
764 | } | 781 | } |
765 | const OBJECT_STORAGE_PROXY_PATHS = { | 782 | const OBJECT_STORAGE_PROXY_PATHS = { |
766 | PRIVATE_WEBSEED: '/object-storage-proxy/webseed/private/', | 783 | // Need to keep this legacy path for previously generated torrents |
784 | LEGACY_PRIVATE_WEB_VIDEOS: '/object-storage-proxy/webseed/private/', | ||
785 | PRIVATE_WEB_VIDEOS: '/object-storage-proxy/web-videos/private/', | ||
767 | 786 | ||
768 | STREAMING_PLAYLISTS: { | 787 | STREAMING_PLAYLISTS: { |
769 | PRIVATE_HLS: '/object-storage-proxy/streaming-playlists/hls/private/' | 788 | PRIVATE_HLS: '/object-storage-proxy/streaming-playlists/hls/private/' |
@@ -807,6 +826,14 @@ const ACTOR_IMAGES_SIZE: { [key in ActorImageType]: { width: number, height: num | |||
807 | ] | 826 | ] |
808 | } | 827 | } |
809 | 828 | ||
829 | const STORYBOARD = { | ||
830 | SPRITE_SIZE: { | ||
831 | width: 192, | ||
832 | height: 108 | ||
833 | }, | ||
834 | SPRITES_MAX_EDGE_COUNT: 10 | ||
835 | } | ||
836 | |||
810 | const EMBED_SIZE = { | 837 | const EMBED_SIZE = { |
811 | width: 560, | 838 | width: 560, |
812 | height: 315 | 839 | height: 315 |
@@ -818,6 +845,10 @@ const FILES_CACHE = { | |||
818 | DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'previews'), | 845 | DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'previews'), |
819 | MAX_AGE: 1000 * 3600 * 3 // 3 hours | 846 | MAX_AGE: 1000 * 3600 * 3 // 3 hours |
820 | }, | 847 | }, |
848 | STORYBOARDS: { | ||
849 | DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'storyboards'), | ||
850 | MAX_AGE: 1000 * 3600 * 24 // 24 hours | ||
851 | }, | ||
821 | VIDEO_CAPTIONS: { | 852 | VIDEO_CAPTIONS: { |
822 | DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'video-captions'), | 853 | DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'video-captions'), |
823 | MAX_AGE: 1000 * 3600 * 3 // 3 hours | 854 | MAX_AGE: 1000 * 3600 * 3 // 3 hours |
@@ -832,8 +863,8 @@ const LRU_CACHE = { | |||
832 | USER_TOKENS: { | 863 | USER_TOKENS: { |
833 | MAX_SIZE: 1000 | 864 | MAX_SIZE: 1000 |
834 | }, | 865 | }, |
835 | ACTOR_IMAGE_STATIC: { | 866 | FILENAME_TO_PATH_PERMANENT_FILE_CACHE: { |
836 | MAX_SIZE: 500 | 867 | MAX_SIZE: 1000 |
837 | }, | 868 | }, |
838 | STATIC_VIDEO_FILES_RIGHTS_CHECK: { | 869 | STATIC_VIDEO_FILES_RIGHTS_CHECK: { |
839 | MAX_SIZE: 5000, | 870 | MAX_SIZE: 5000, |
@@ -857,8 +888,8 @@ const DIRECTORIES = { | |||
857 | }, | 888 | }, |
858 | 889 | ||
859 | VIDEOS: { | 890 | VIDEOS: { |
860 | PUBLIC: CONFIG.STORAGE.VIDEOS_DIR, | 891 | PUBLIC: CONFIG.STORAGE.WEB_VIDEOS_DIR, |
861 | PRIVATE: join(CONFIG.STORAGE.VIDEOS_DIR, 'private') | 892 | PRIVATE: join(CONFIG.STORAGE.WEB_VIDEOS_DIR, 'private') |
862 | }, | 893 | }, |
863 | 894 | ||
864 | HLS_REDUNDANCY: join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') | 895 | HLS_REDUNDANCY: join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') |
@@ -1084,6 +1115,7 @@ export { | |||
1084 | RESUMABLE_UPLOAD_SESSION_LIFETIME, | 1115 | RESUMABLE_UPLOAD_SESSION_LIFETIME, |
1085 | RUNNER_JOB_STATES, | 1116 | RUNNER_JOB_STATES, |
1086 | P2P_MEDIA_LOADER_PEER_VERSION, | 1117 | P2P_MEDIA_LOADER_PEER_VERSION, |
1118 | STORYBOARD, | ||
1087 | ACTOR_IMAGES_SIZE, | 1119 | ACTOR_IMAGES_SIZE, |
1088 | ACCEPT_HEADERS, | 1120 | ACCEPT_HEADERS, |
1089 | BCRYPT_SALT_SIZE, | 1121 | BCRYPT_SALT_SIZE, |
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 14dd8c379..bc120e398 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -10,6 +10,7 @@ import { UserModel } from '@server/models/user/user' | |||
10 | import { UserNotificationModel } from '@server/models/user/user-notification' | 10 | import { UserNotificationModel } from '@server/models/user/user-notification' |
11 | import { UserRegistrationModel } from '@server/models/user/user-registration' | 11 | import { UserRegistrationModel } from '@server/models/user/user-registration' |
12 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' | 12 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' |
13 | import { StoryboardModel } from '@server/models/video/storyboard' | ||
13 | import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' | 14 | import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' |
14 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' | 15 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' |
15 | import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' | 16 | import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' |
@@ -56,6 +57,7 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla | |||
56 | import { VideoTagModel } from '../models/video/video-tag' | 57 | import { VideoTagModel } from '../models/video/video-tag' |
57 | import { VideoViewModel } from '../models/view/video-view' | 58 | import { VideoViewModel } from '../models/view/video-view' |
58 | import { CONFIG } from './config' | 59 | import { CONFIG } from './config' |
60 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
59 | 61 | ||
60 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 62 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
61 | 63 | ||
@@ -163,9 +165,11 @@ async function initDatabaseModels (silent: boolean) { | |||
163 | VideoJobInfoModel, | 165 | VideoJobInfoModel, |
164 | VideoChannelSyncModel, | 166 | VideoChannelSyncModel, |
165 | UserRegistrationModel, | 167 | UserRegistrationModel, |
168 | VideoPasswordModel, | ||
166 | RunnerRegistrationTokenModel, | 169 | RunnerRegistrationTokenModel, |
167 | RunnerModel, | 170 | RunnerModel, |
168 | RunnerJobModel | 171 | RunnerJobModel, |
172 | StoryboardModel | ||
169 | ]) | 173 | ]) |
170 | 174 | ||
171 | // Check extensions exist in the database | 175 | // Check extensions exist in the database |
diff --git a/server/initializers/migrations/0785-video-password-protection.ts b/server/initializers/migrations/0785-video-password-protection.ts new file mode 100644 index 000000000..1d85f4489 --- /dev/null +++ b/server/initializers/migrations/0785-video-password-protection.ts | |||
@@ -0,0 +1,31 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | { | ||
9 | const query = ` | ||
10 | CREATE TABLE IF NOT EXISTS "videoPassword" ( | ||
11 | "id" SERIAL, | ||
12 | "password" VARCHAR(255) NOT NULL, | ||
13 | "videoId" INTEGER NOT NULL REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE, | ||
14 | "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
15 | "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
16 | PRIMARY KEY ("id") | ||
17 | ); | ||
18 | ` | ||
19 | |||
20 | await utils.sequelize.query(query, { transaction : utils.transaction }) | ||
21 | } | ||
22 | } | ||
23 | |||
24 | function down (options) { | ||
25 | throw new Error('Not implemented.') | ||
26 | } | ||
27 | |||
28 | export { | ||
29 | up, | ||
30 | down | ||
31 | } | ||
diff --git a/server/initializers/migrations/0790-thumbnail-disk.ts b/server/initializers/migrations/0790-thumbnail-disk.ts new file mode 100644 index 000000000..0824c042e --- /dev/null +++ b/server/initializers/migrations/0790-thumbnail-disk.ts | |||
@@ -0,0 +1,47 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | const { transaction } = utils | ||
9 | |||
10 | { | ||
11 | const data = { | ||
12 | type: Sequelize.BOOLEAN, | ||
13 | allowNull: true, | ||
14 | defaultValue: true | ||
15 | } | ||
16 | |||
17 | await utils.queryInterface.addColumn('thumbnail', 'onDisk', data, { transaction }) | ||
18 | } | ||
19 | |||
20 | { | ||
21 | // Remote previews are not on the disk | ||
22 | await utils.sequelize.query( | ||
23 | 'UPDATE "thumbnail" SET "onDisk" = FALSE ' + | ||
24 | 'WHERE "type" = 2 AND "videoId" NOT IN (SELECT "id" FROM "video" WHERE "remote" IS FALSE)', | ||
25 | { transaction } | ||
26 | ) | ||
27 | } | ||
28 | |||
29 | { | ||
30 | const data = { | ||
31 | type: Sequelize.BOOLEAN, | ||
32 | allowNull: false, | ||
33 | defaultValue: null | ||
34 | } | ||
35 | |||
36 | await utils.queryInterface.changeColumn('thumbnail', 'onDisk', data, { transaction }) | ||
37 | } | ||
38 | } | ||
39 | |||
40 | function down (options) { | ||
41 | throw new Error('Not implemented.') | ||
42 | } | ||
43 | |||
44 | export { | ||
45 | up, | ||
46 | down | ||
47 | } | ||
diff --git a/server/lib/activitypub/activity.ts b/server/lib/activitypub/activity.ts index 1f6ec221e..0fed3e8fd 100644 --- a/server/lib/activitypub/activity.ts +++ b/server/lib/activitypub/activity.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { ActivityType } from '@shared/models' | 1 | import { doJSONRequest } from '@server/helpers/requests' |
2 | import { APObjectId, ActivityObject, ActivityPubActor, ActivityType } from '@shared/models' | ||
2 | 3 | ||
3 | function getAPId (object: string | { id: string }) { | 4 | function getAPId (object: string | { id: string }) { |
4 | if (typeof object === 'string') return object | 5 | if (typeof object === 'string') return object |
@@ -32,8 +33,19 @@ function buildAvailableActivities (): ActivityType[] { | |||
32 | ] | 33 | ] |
33 | } | 34 | } |
34 | 35 | ||
36 | async function fetchAPObject <T extends (ActivityObject | ActivityPubActor)> (object: APObjectId) { | ||
37 | if (typeof object === 'string') { | ||
38 | const { body } = await doJSONRequest<Exclude<T, string>>(object, { activityPub: true }) | ||
39 | |||
40 | return body | ||
41 | } | ||
42 | |||
43 | return object as Exclude<T, string> | ||
44 | } | ||
45 | |||
35 | export { | 46 | export { |
36 | getAPId, | 47 | getAPId, |
48 | fetchAPObject, | ||
37 | getActivityStreamDuration, | 49 | getActivityStreamDuration, |
38 | buildAvailableActivities, | 50 | buildAvailableActivities, |
39 | getDurationFromActivityStream | 51 | getDurationFromActivityStream |
diff --git a/server/lib/activitypub/actors/get.ts b/server/lib/activitypub/actors/get.ts index e73b7d707..b2be3f5fb 100644 --- a/server/lib/activitypub/actors/get.ts +++ b/server/lib/activitypub/actors/get.ts | |||
@@ -3,8 +3,9 @@ import { logger } from '@server/helpers/logger' | |||
3 | import { JobQueue } from '@server/lib/job-queue' | 3 | import { JobQueue } from '@server/lib/job-queue' |
4 | import { ActorLoadByUrlType, loadActorByUrl } from '@server/lib/model-loaders' | 4 | import { ActorLoadByUrlType, loadActorByUrl } from '@server/lib/model-loaders' |
5 | import { MActor, MActorAccountChannelId, MActorAccountChannelIdActor, MActorAccountId, MActorFullActor } from '@server/types/models' | 5 | import { MActor, MActorAccountChannelId, MActorAccountChannelIdActor, MActorAccountId, MActorFullActor } from '@server/types/models' |
6 | import { ActivityPubActor } from '@shared/models' | 6 | import { arrayify } from '@shared/core-utils' |
7 | import { getAPId } from '../activity' | 7 | import { ActivityPubActor, APObjectId } from '@shared/models' |
8 | import { fetchAPObject, getAPId } from '../activity' | ||
8 | import { checkUrlsSameHost } from '../url' | 9 | import { checkUrlsSameHost } from '../url' |
9 | import { refreshActorIfNeeded } from './refresh' | 10 | import { refreshActorIfNeeded } from './refresh' |
10 | import { APActorCreator, fetchRemoteActor } from './shared' | 11 | import { APActorCreator, fetchRemoteActor } from './shared' |
@@ -40,7 +41,7 @@ async function getOrCreateAPActor ( | |||
40 | const { actorObject } = await fetchRemoteActor(actorUrl) | 41 | const { actorObject } = await fetchRemoteActor(actorUrl) |
41 | if (actorObject === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl) | 42 | if (actorObject === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl) |
42 | 43 | ||
43 | // actorUrl is just an alias/rediraction, so process object id instead | 44 | // actorUrl is just an alias/redirection, so process object id instead |
44 | if (actorObject.id !== actorUrl) return getOrCreateAPActor(actorObject, 'all', recurseIfNeeded, updateCollections) | 45 | if (actorObject.id !== actorUrl) return getOrCreateAPActor(actorObject, 'all', recurseIfNeeded, updateCollections) |
45 | 46 | ||
46 | // Create the attributed to actor | 47 | // Create the attributed to actor |
@@ -68,29 +69,48 @@ async function getOrCreateAPActor ( | |||
68 | return actorRefreshed | 69 | return actorRefreshed |
69 | } | 70 | } |
70 | 71 | ||
71 | function getOrCreateAPOwner (actorObject: ActivityPubActor, actorUrl: string) { | 72 | async function getOrCreateAPOwner (actorObject: ActivityPubActor, actorUrl: string) { |
72 | const accountAttributedTo = actorObject.attributedTo.find(a => a.type === 'Person') | 73 | const accountAttributedTo = await findOwner(actorUrl, actorObject.attributedTo, 'Person') |
73 | if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actorUrl) | 74 | if (!accountAttributedTo) { |
74 | 75 | throw new Error(`Cannot find account attributed to video channel ${actorUrl}`) | |
75 | if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) { | ||
76 | throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`) | ||
77 | } | 76 | } |
78 | 77 | ||
79 | try { | 78 | try { |
80 | // Don't recurse another time | 79 | // Don't recurse another time |
81 | const recurseIfNeeded = false | 80 | const recurseIfNeeded = false |
82 | return getOrCreateAPActor(accountAttributedTo.id, 'all', recurseIfNeeded) | 81 | return getOrCreateAPActor(accountAttributedTo, 'all', recurseIfNeeded) |
83 | } catch (err) { | 82 | } catch (err) { |
84 | logger.error('Cannot get or create account attributed to video channel ' + actorUrl) | 83 | logger.error('Cannot get or create account attributed to video channel ' + actorUrl) |
85 | throw new Error(err) | 84 | throw new Error(err) |
86 | } | 85 | } |
87 | } | 86 | } |
88 | 87 | ||
88 | async function findOwner (rootUrl: string, attributedTo: APObjectId[] | APObjectId, type: 'Person' | 'Group') { | ||
89 | for (const actorToCheck of arrayify(attributedTo)) { | ||
90 | const actorObject = await fetchAPObject<ActivityPubActor>(getAPId(actorToCheck)) | ||
91 | |||
92 | if (!actorObject) { | ||
93 | logger.warn('Unknown attributed to actor %s for owner %s', actorToCheck, rootUrl) | ||
94 | continue | ||
95 | } | ||
96 | |||
97 | if (checkUrlsSameHost(actorObject.id, rootUrl) !== true) { | ||
98 | logger.warn(`Account attributed to ${actorObject.id} does not have the same host than owner actor url ${rootUrl}`) | ||
99 | continue | ||
100 | } | ||
101 | |||
102 | if (actorObject.type === type) return actorObject | ||
103 | } | ||
104 | |||
105 | return undefined | ||
106 | } | ||
107 | |||
89 | // --------------------------------------------------------------------------- | 108 | // --------------------------------------------------------------------------- |
90 | 109 | ||
91 | export { | 110 | export { |
92 | getOrCreateAPOwner, | 111 | getOrCreateAPOwner, |
93 | getOrCreateAPActor | 112 | getOrCreateAPActor, |
113 | findOwner | ||
94 | } | 114 | } |
95 | 115 | ||
96 | // --------------------------------------------------------------------------- | 116 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/activitypub/actors/refresh.ts b/server/lib/activitypub/actors/refresh.ts index 6d8428d66..d15cb5e90 100644 --- a/server/lib/activitypub/actors/refresh.ts +++ b/server/lib/activitypub/actors/refresh.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
2 | import { PromiseCache } from '@server/helpers/promise-cache' | 2 | import { CachePromiseFactory } from '@server/helpers/promise-cache' |
3 | import { PeerTubeRequestError } from '@server/helpers/requests' | 3 | import { PeerTubeRequestError } from '@server/helpers/requests' |
4 | import { ActorLoadByUrlType } from '@server/lib/model-loaders' | 4 | import { ActorLoadByUrlType } from '@server/lib/model-loaders' |
5 | import { ActorModel } from '@server/models/actor/actor' | 5 | import { ActorModel } from '@server/models/actor/actor' |
@@ -16,7 +16,7 @@ type RefreshOptions <T> = { | |||
16 | fetchedType: ActorLoadByUrlType | 16 | fetchedType: ActorLoadByUrlType |
17 | } | 17 | } |
18 | 18 | ||
19 | const promiseCache = new PromiseCache(doRefresh, (options: RefreshOptions<MActorFull | MActorAccountChannelId>) => options.actor.url) | 19 | const promiseCache = new CachePromiseFactory(doRefresh, (options: RefreshOptions<MActorFull | MActorAccountChannelId>) => options.actor.url) |
20 | 20 | ||
21 | function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannelId> (options: RefreshOptions<T>): RefreshResult <T> { | 21 | function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannelId> (options: RefreshOptions<T>): RefreshResult <T> { |
22 | const actorArg = options.actor | 22 | const actorArg = options.actor |
diff --git a/server/lib/activitypub/context.ts b/server/lib/activitypub/context.ts index a3ca52a31..750276a11 100644 --- a/server/lib/activitypub/context.ts +++ b/server/lib/activitypub/context.ts | |||
@@ -46,6 +46,19 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string | |||
46 | 46 | ||
47 | Infohash: 'pt:Infohash', | 47 | Infohash: 'pt:Infohash', |
48 | 48 | ||
49 | tileWidth: { | ||
50 | '@type': 'sc:Number', | ||
51 | '@id': 'pt:tileWidth' | ||
52 | }, | ||
53 | tileHeight: { | ||
54 | '@type': 'sc:Number', | ||
55 | '@id': 'pt:tileHeight' | ||
56 | }, | ||
57 | tileDuration: { | ||
58 | '@type': 'sc:Number', | ||
59 | '@id': 'pt:tileDuration' | ||
60 | }, | ||
61 | |||
49 | originallyPublishedAt: 'sc:datePublished', | 62 | originallyPublishedAt: 'sc:datePublished', |
50 | views: { | 63 | views: { |
51 | '@type': 'sc:Number', | 64 | '@type': 'sc:Number', |
diff --git a/server/lib/activitypub/playlists/create-update.ts b/server/lib/activitypub/playlists/create-update.ts index 9339e8ea4..b24299f29 100644 --- a/server/lib/activitypub/playlists/create-update.ts +++ b/server/lib/activitypub/playlists/create-update.ts | |||
@@ -4,7 +4,7 @@ import { retryTransactionWrapper } from '@server/helpers/database-utils' | |||
4 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 4 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
5 | import { CRAWL_REQUEST_CONCURRENCY } from '@server/initializers/constants' | 5 | import { CRAWL_REQUEST_CONCURRENCY } from '@server/initializers/constants' |
6 | import { sequelizeTypescript } from '@server/initializers/database' | 6 | import { sequelizeTypescript } from '@server/initializers/database' |
7 | import { updatePlaylistMiniatureFromUrl } from '@server/lib/thumbnail' | 7 | import { updateRemotePlaylistMiniatureFromUrl } from '@server/lib/thumbnail' |
8 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | 8 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' |
9 | import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element' | 9 | import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element' |
10 | import { FilteredModelAttributes } from '@server/types' | 10 | import { FilteredModelAttributes } from '@server/types' |
@@ -77,7 +77,7 @@ async function setVideoChannel (playlistObject: PlaylistObject, playlistAttribut | |||
77 | throw new Error('Not attributed to for playlist object ' + getAPId(playlistObject)) | 77 | throw new Error('Not attributed to for playlist object ' + getAPId(playlistObject)) |
78 | } | 78 | } |
79 | 79 | ||
80 | const actor = await getOrCreateAPActor(playlistObject.attributedTo[0], 'all') | 80 | const actor = await getOrCreateAPActor(getAPId(playlistObject.attributedTo[0]), 'all') |
81 | 81 | ||
82 | if (!actor.VideoChannel) { | 82 | if (!actor.VideoChannel) { |
83 | logger.warn('Playlist "attributedTo" %s is not a video channel.', playlistObject.id, { playlistObject, ...lTags(playlistObject.id) }) | 83 | logger.warn('Playlist "attributedTo" %s is not a video channel.', playlistObject.id, { playlistObject, ...lTags(playlistObject.id) }) |
@@ -104,7 +104,7 @@ async function updatePlaylistThumbnail (playlistObject: PlaylistObject, playlist | |||
104 | let thumbnailModel: MThumbnail | 104 | let thumbnailModel: MThumbnail |
105 | 105 | ||
106 | try { | 106 | try { |
107 | thumbnailModel = await updatePlaylistMiniatureFromUrl({ downloadUrl: playlistObject.icon.url, playlist }) | 107 | thumbnailModel = await updateRemotePlaylistMiniatureFromUrl({ downloadUrl: playlistObject.icon.url, playlist }) |
108 | await playlist.setAndSaveThumbnail(thumbnailModel, undefined) | 108 | await playlist.setAndSaveThumbnail(thumbnailModel, undefined) |
109 | } catch (err) { | 109 | } catch (err) { |
110 | logger.warn('Cannot set thumbnail of %s.', playlistObject.id, { err, ...lTags(playlistObject.id, playlist.uuid, playlist.url) }) | 110 | logger.warn('Cannot set thumbnail of %s.', playlistObject.id, { err, ...lTags(playlistObject.id, playlist.uuid, playlist.url) }) |
diff --git a/server/lib/activitypub/playlists/get.ts b/server/lib/activitypub/playlists/get.ts index bfaf52cc9..c34554d69 100644 --- a/server/lib/activitypub/playlists/get.ts +++ b/server/lib/activitypub/playlists/get.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | 1 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' |
2 | import { MVideoPlaylistFullSummary } from '@server/types/models' | 2 | import { MVideoPlaylistFullSummary } from '@server/types/models' |
3 | import { APObject } from '@shared/models' | 3 | import { APObjectId } from '@shared/models' |
4 | import { getAPId } from '../activity' | 4 | import { getAPId } from '../activity' |
5 | import { createOrUpdateVideoPlaylist } from './create-update' | 5 | import { createOrUpdateVideoPlaylist } from './create-update' |
6 | import { scheduleRefreshIfNeeded } from './refresh' | 6 | import { scheduleRefreshIfNeeded } from './refresh' |
7 | import { fetchRemoteVideoPlaylist } from './shared' | 7 | import { fetchRemoteVideoPlaylist } from './shared' |
8 | 8 | ||
9 | async function getOrCreateAPVideoPlaylist (playlistObjectArg: APObject): Promise<MVideoPlaylistFullSummary> { | 9 | async function getOrCreateAPVideoPlaylist (playlistObjectArg: APObjectId): Promise<MVideoPlaylistFullSummary> { |
10 | const playlistUrl = getAPId(playlistObjectArg) | 10 | const playlistUrl = getAPId(playlistObjectArg) |
11 | 11 | ||
12 | const playlistFromDatabase = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(playlistUrl) | 12 | const playlistFromDatabase = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(playlistUrl) |
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 1e6e8956c..e89d1ab45 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -1,13 +1,24 @@ | |||
1 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | 1 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' |
2 | import { isRedundancyAccepted } from '@server/lib/redundancy' | 2 | import { isRedundancyAccepted } from '@server/lib/redundancy' |
3 | import { VideoModel } from '@server/models/video/video' | 3 | import { VideoModel } from '@server/models/video/video' |
4 | import { ActivityCreate, CacheFileObject, PlaylistObject, VideoCommentObject, VideoObject, WatchActionObject } from '@shared/models' | 4 | import { |
5 | AbuseObject, | ||
6 | ActivityCreate, | ||
7 | ActivityCreateObject, | ||
8 | ActivityObject, | ||
9 | CacheFileObject, | ||
10 | PlaylistObject, | ||
11 | VideoCommentObject, | ||
12 | VideoObject, | ||
13 | WatchActionObject | ||
14 | } from '@shared/models' | ||
5 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 15 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
6 | import { logger } from '../../../helpers/logger' | 16 | import { logger } from '../../../helpers/logger' |
7 | import { sequelizeTypescript } from '../../../initializers/database' | 17 | import { sequelizeTypescript } from '../../../initializers/database' |
8 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 18 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
9 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' | 19 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' |
10 | import { Notifier } from '../../notifier' | 20 | import { Notifier } from '../../notifier' |
21 | import { fetchAPObject } from '../activity' | ||
11 | import { createOrUpdateCacheFile } from '../cache-file' | 22 | import { createOrUpdateCacheFile } from '../cache-file' |
12 | import { createOrUpdateLocalVideoViewer } from '../local-video-viewer' | 23 | import { createOrUpdateLocalVideoViewer } from '../local-video-viewer' |
13 | import { createOrUpdateVideoPlaylist } from '../playlists' | 24 | import { createOrUpdateVideoPlaylist } from '../playlists' |
@@ -15,35 +26,35 @@ import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | |||
15 | import { resolveThread } from '../video-comments' | 26 | import { resolveThread } from '../video-comments' |
16 | import { getOrCreateAPVideo } from '../videos' | 27 | import { getOrCreateAPVideo } from '../videos' |
17 | 28 | ||
18 | async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { | 29 | async function processCreateActivity (options: APProcessorOptions<ActivityCreate<ActivityCreateObject>>) { |
19 | const { activity, byActor } = options | 30 | const { activity, byActor } = options |
20 | 31 | ||
21 | // Only notify if it is not from a fetcher job | 32 | // Only notify if it is not from a fetcher job |
22 | const notify = options.fromFetch !== true | 33 | const notify = options.fromFetch !== true |
23 | const activityObject = activity.object | 34 | const activityObject = await fetchAPObject<Exclude<ActivityObject, AbuseObject>>(activity.object) |
24 | const activityType = activityObject.type | 35 | const activityType = activityObject.type |
25 | 36 | ||
26 | if (activityType === 'Video') { | 37 | if (activityType === 'Video') { |
27 | return processCreateVideo(activity, notify) | 38 | return processCreateVideo(activityObject, notify) |
28 | } | 39 | } |
29 | 40 | ||
30 | if (activityType === 'Note') { | 41 | if (activityType === 'Note') { |
31 | // Comments will be fetched from videos | 42 | // Comments will be fetched from videos |
32 | if (options.fromFetch) return | 43 | if (options.fromFetch) return |
33 | 44 | ||
34 | return retryTransactionWrapper(processCreateVideoComment, activity, byActor, notify) | 45 | return retryTransactionWrapper(processCreateVideoComment, activity, activityObject, byActor, notify) |
35 | } | 46 | } |
36 | 47 | ||
37 | if (activityType === 'WatchAction') { | 48 | if (activityType === 'WatchAction') { |
38 | return retryTransactionWrapper(processCreateWatchAction, activity) | 49 | return retryTransactionWrapper(processCreateWatchAction, activityObject) |
39 | } | 50 | } |
40 | 51 | ||
41 | if (activityType === 'CacheFile') { | 52 | if (activityType === 'CacheFile') { |
42 | return retryTransactionWrapper(processCreateCacheFile, activity, byActor) | 53 | return retryTransactionWrapper(processCreateCacheFile, activity, activityObject, byActor) |
43 | } | 54 | } |
44 | 55 | ||
45 | if (activityType === 'Playlist') { | 56 | if (activityType === 'Playlist') { |
46 | return retryTransactionWrapper(processCreatePlaylist, activity, byActor) | 57 | return retryTransactionWrapper(processCreatePlaylist, activity, activityObject, byActor) |
47 | } | 58 | } |
48 | 59 | ||
49 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) | 60 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) |
@@ -58,9 +69,7 @@ export { | |||
58 | 69 | ||
59 | // --------------------------------------------------------------------------- | 70 | // --------------------------------------------------------------------------- |
60 | 71 | ||
61 | async function processCreateVideo (activity: ActivityCreate, notify: boolean) { | 72 | async function processCreateVideo (videoToCreateData: VideoObject, notify: boolean) { |
62 | const videoToCreateData = activity.object as VideoObject | ||
63 | |||
64 | const syncParam = { rates: false, shares: false, comments: false, thumbnail: true, refreshVideo: false } | 73 | const syncParam = { rates: false, shares: false, comments: false, thumbnail: true, refreshVideo: false } |
65 | const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam }) | 74 | const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam }) |
66 | 75 | ||
@@ -69,11 +78,13 @@ async function processCreateVideo (activity: ActivityCreate, notify: boolean) { | |||
69 | return video | 78 | return video |
70 | } | 79 | } |
71 | 80 | ||
72 | async function processCreateCacheFile (activity: ActivityCreate, byActor: MActorSignature) { | 81 | async function processCreateCacheFile ( |
82 | activity: ActivityCreate<CacheFileObject | string>, | ||
83 | cacheFile: CacheFileObject, | ||
84 | byActor: MActorSignature | ||
85 | ) { | ||
73 | if (await isRedundancyAccepted(activity, byActor) !== true) return | 86 | if (await isRedundancyAccepted(activity, byActor) !== true) return |
74 | 87 | ||
75 | const cacheFile = activity.object as CacheFileObject | ||
76 | |||
77 | const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object }) | 88 | const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object }) |
78 | 89 | ||
79 | await sequelizeTypescript.transaction(async t => { | 90 | await sequelizeTypescript.transaction(async t => { |
@@ -87,9 +98,7 @@ async function processCreateCacheFile (activity: ActivityCreate, byActor: MActor | |||
87 | } | 98 | } |
88 | } | 99 | } |
89 | 100 | ||
90 | async function processCreateWatchAction (activity: ActivityCreate) { | 101 | async function processCreateWatchAction (watchAction: WatchActionObject) { |
91 | const watchAction = activity.object as WatchActionObject | ||
92 | |||
93 | if (watchAction.actionStatus !== 'CompletedActionStatus') return | 102 | if (watchAction.actionStatus !== 'CompletedActionStatus') return |
94 | 103 | ||
95 | const video = await VideoModel.loadByUrl(watchAction.object) | 104 | const video = await VideoModel.loadByUrl(watchAction.object) |
@@ -100,8 +109,12 @@ async function processCreateWatchAction (activity: ActivityCreate) { | |||
100 | }) | 109 | }) |
101 | } | 110 | } |
102 | 111 | ||
103 | async function processCreateVideoComment (activity: ActivityCreate, byActor: MActorSignature, notify: boolean) { | 112 | async function processCreateVideoComment ( |
104 | const commentObject = activity.object as VideoCommentObject | 113 | activity: ActivityCreate<VideoCommentObject | string>, |
114 | commentObject: VideoCommentObject, | ||
115 | byActor: MActorSignature, | ||
116 | notify: boolean | ||
117 | ) { | ||
105 | const byAccount = byActor.Account | 118 | const byAccount = byActor.Account |
106 | 119 | ||
107 | if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url) | 120 | if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url) |
@@ -144,8 +157,11 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: MAc | |||
144 | if (created && notify) Notifier.Instance.notifyOnNewComment(comment) | 157 | if (created && notify) Notifier.Instance.notifyOnNewComment(comment) |
145 | } | 158 | } |
146 | 159 | ||
147 | async function processCreatePlaylist (activity: ActivityCreate, byActor: MActorSignature) { | 160 | async function processCreatePlaylist ( |
148 | const playlistObject = activity.object as PlaylistObject | 161 | activity: ActivityCreate<PlaylistObject | string>, |
162 | playlistObject: PlaylistObject, | ||
163 | byActor: MActorSignature | ||
164 | ) { | ||
149 | const byAccount = byActor.Account | 165 | const byAccount = byActor.Account |
150 | 166 | ||
151 | if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url) | 167 | if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url) |
diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts index 44e349b22..4e270f917 100644 --- a/server/lib/activitypub/process/process-dislike.ts +++ b/server/lib/activitypub/process/process-dislike.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { VideoModel } from '@server/models/video/video' | 1 | import { VideoModel } from '@server/models/video/video' |
2 | import { ActivityCreate, ActivityDislike, DislikeObject } from '@shared/models' | 2 | import { ActivityDislike } from '@shared/models' |
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
4 | import { sequelizeTypescript } from '../../../initializers/database' | 4 | import { sequelizeTypescript } from '../../../initializers/database' |
5 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 5 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
@@ -7,7 +7,7 @@ import { APProcessorOptions } from '../../../types/activitypub-processor.model' | |||
7 | import { MActorSignature } from '../../../types/models' | 7 | import { MActorSignature } from '../../../types/models' |
8 | import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' | 8 | import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' |
9 | 9 | ||
10 | async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) { | 10 | async function processDislikeActivity (options: APProcessorOptions<ActivityDislike>) { |
11 | const { activity, byActor } = options | 11 | const { activity, byActor } = options |
12 | return retryTransactionWrapper(processDislike, activity, byActor) | 12 | return retryTransactionWrapper(processDislike, activity, byActor) |
13 | } | 13 | } |
@@ -20,11 +20,8 @@ export { | |||
20 | 20 | ||
21 | // --------------------------------------------------------------------------- | 21 | // --------------------------------------------------------------------------- |
22 | 22 | ||
23 | async function processDislike (activity: ActivityCreate | ActivityDislike, byActor: MActorSignature) { | 23 | async function processDislike (activity: ActivityDislike, byActor: MActorSignature) { |
24 | const dislikeObject = activity.type === 'Dislike' | 24 | const dislikeObject = activity.object |
25 | ? activity.object | ||
26 | : (activity.object as DislikeObject).object | ||
27 | |||
28 | const byAccount = byActor.Account | 25 | const byAccount = byActor.Account |
29 | 26 | ||
30 | if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | 27 | if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) |
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts index 10f58ef27..bea285670 100644 --- a/server/lib/activitypub/process/process-flag.ts +++ b/server/lib/activitypub/process/process-flag.ts | |||
@@ -3,7 +3,7 @@ import { AccountModel } from '@server/models/account/account' | |||
3 | import { VideoModel } from '@server/models/video/video' | 3 | import { VideoModel } from '@server/models/video/video' |
4 | import { VideoCommentModel } from '@server/models/video/video-comment' | 4 | import { VideoCommentModel } from '@server/models/video/video-comment' |
5 | import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' | 5 | import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' |
6 | import { AbuseObject, AbuseState, ActivityCreate, ActivityFlag } from '@shared/models' | 6 | import { AbuseState, ActivityFlag } from '@shared/models' |
7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
8 | import { logger } from '../../../helpers/logger' | 8 | import { logger } from '../../../helpers/logger' |
9 | import { sequelizeTypescript } from '../../../initializers/database' | 9 | import { sequelizeTypescript } from '../../../initializers/database' |
@@ -11,7 +11,7 @@ import { getAPId } from '../../../lib/activitypub/activity' | |||
11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
12 | import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models' | 12 | import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models' |
13 | 13 | ||
14 | async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { | 14 | async function processFlagActivity (options: APProcessorOptions<ActivityFlag>) { |
15 | const { activity, byActor } = options | 15 | const { activity, byActor } = options |
16 | 16 | ||
17 | return retryTransactionWrapper(processCreateAbuse, activity, byActor) | 17 | return retryTransactionWrapper(processCreateAbuse, activity, byActor) |
@@ -25,9 +25,7 @@ export { | |||
25 | 25 | ||
26 | // --------------------------------------------------------------------------- | 26 | // --------------------------------------------------------------------------- |
27 | 27 | ||
28 | async function processCreateAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) { | 28 | async function processCreateAbuse (flag: ActivityFlag, byActor: MActorSignature) { |
29 | const flag = activity.type === 'Flag' ? activity : (activity.object as AbuseObject) | ||
30 | |||
31 | const account = byActor.Account | 29 | const account = byActor.Account |
32 | if (!account) throw new Error('Cannot create abuse with the non account actor ' + byActor.url) | 30 | if (!account) throw new Error('Cannot create abuse with the non account actor ' + byActor.url) |
33 | 31 | ||
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts index 99423a72b..25f68724d 100644 --- a/server/lib/activitypub/process/process-undo.ts +++ b/server/lib/activitypub/process/process-undo.ts | |||
@@ -1,6 +1,14 @@ | |||
1 | import { VideoModel } from '@server/models/video/video' | 1 | import { VideoModel } from '@server/models/video/video' |
2 | import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo, CacheFileObject } from '../../../../shared/models/activitypub' | 2 | import { |
3 | import { DislikeObject } from '../../../../shared/models/activitypub/objects' | 3 | ActivityAnnounce, |
4 | ActivityCreate, | ||
5 | ActivityDislike, | ||
6 | ActivityFollow, | ||
7 | ActivityLike, | ||
8 | ActivityUndo, | ||
9 | ActivityUndoObject, | ||
10 | CacheFileObject | ||
11 | } from '../../../../shared/models/activitypub' | ||
4 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 12 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
5 | import { logger } from '../../../helpers/logger' | 13 | import { logger } from '../../../helpers/logger' |
6 | import { sequelizeTypescript } from '../../../initializers/database' | 14 | import { sequelizeTypescript } from '../../../initializers/database' |
@@ -11,10 +19,11 @@ import { VideoRedundancyModel } from '../../../models/redundancy/video-redundanc | |||
11 | import { VideoShareModel } from '../../../models/video/video-share' | 19 | import { VideoShareModel } from '../../../models/video/video-share' |
12 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 20 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
13 | import { MActorSignature } from '../../../types/models' | 21 | import { MActorSignature } from '../../../types/models' |
22 | import { fetchAPObject } from '../activity' | ||
14 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | 23 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' |
15 | import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' | 24 | import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' |
16 | 25 | ||
17 | async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) { | 26 | async function processUndoActivity (options: APProcessorOptions<ActivityUndo<ActivityUndoObject>>) { |
18 | const { activity, byActor } = options | 27 | const { activity, byActor } = options |
19 | const activityToUndo = activity.object | 28 | const activityToUndo = activity.object |
20 | 29 | ||
@@ -23,8 +32,10 @@ async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) { | |||
23 | } | 32 | } |
24 | 33 | ||
25 | if (activityToUndo.type === 'Create') { | 34 | if (activityToUndo.type === 'Create') { |
26 | if (activityToUndo.object.type === 'CacheFile') { | 35 | const objectToUndo = await fetchAPObject<CacheFileObject>(activityToUndo.object) |
27 | return retryTransactionWrapper(processUndoCacheFile, byActor, activity) | 36 | |
37 | if (objectToUndo.type === 'CacheFile') { | ||
38 | return retryTransactionWrapper(processUndoCacheFile, byActor, activity, objectToUndo) | ||
28 | } | 39 | } |
29 | } | 40 | } |
30 | 41 | ||
@@ -53,8 +64,8 @@ export { | |||
53 | 64 | ||
54 | // --------------------------------------------------------------------------- | 65 | // --------------------------------------------------------------------------- |
55 | 66 | ||
56 | async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) { | 67 | async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo<ActivityLike>) { |
57 | const likeActivity = activity.object as ActivityLike | 68 | const likeActivity = activity.object |
58 | 69 | ||
59 | const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: likeActivity.object }) | 70 | const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: likeActivity.object }) |
60 | // We don't care about likes of remote videos | 71 | // We don't care about likes of remote videos |
@@ -78,12 +89,10 @@ async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo | |||
78 | }) | 89 | }) |
79 | } | 90 | } |
80 | 91 | ||
81 | async function processUndoDislike (byActor: MActorSignature, activity: ActivityUndo) { | 92 | async function processUndoDislike (byActor: MActorSignature, activity: ActivityUndo<ActivityDislike>) { |
82 | const dislike = activity.object.type === 'Dislike' | 93 | const dislikeActivity = activity.object |
83 | ? activity.object | ||
84 | : activity.object.object as DislikeObject | ||
85 | 94 | ||
86 | const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislike.object }) | 95 | const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislikeActivity.object }) |
87 | // We don't care about likes of remote videos | 96 | // We don't care about likes of remote videos |
88 | if (!onlyVideo.isOwned()) return | 97 | if (!onlyVideo.isOwned()) return |
89 | 98 | ||
@@ -91,7 +100,7 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU | |||
91 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) | 100 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) |
92 | 101 | ||
93 | const video = await VideoModel.loadFull(onlyVideo.id, t) | 102 | const video = await VideoModel.loadFull(onlyVideo.id, t) |
94 | const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, dislike.id, t) | 103 | const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, dislikeActivity.id, t) |
95 | if (!rate || rate.type !== 'dislike') { | 104 | if (!rate || rate.type !== 'dislike') { |
96 | logger.warn(`Unknown dislike by account %d for video %d.`, byActor.Account.id, video.id) | 105 | logger.warn(`Unknown dislike by account %d for video %d.`, byActor.Account.id, video.id) |
97 | return | 106 | return |
@@ -107,9 +116,11 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU | |||
107 | 116 | ||
108 | // --------------------------------------------------------------------------- | 117 | // --------------------------------------------------------------------------- |
109 | 118 | ||
110 | async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) { | 119 | async function processUndoCacheFile ( |
111 | const cacheFileObject = activity.object.object as CacheFileObject | 120 | byActor: MActorSignature, |
112 | 121 | activity: ActivityUndo<ActivityCreate<CacheFileObject>>, | |
122 | cacheFileObject: CacheFileObject | ||
123 | ) { | ||
113 | const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object }) | 124 | const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object }) |
114 | 125 | ||
115 | return sequelizeTypescript.transaction(async t => { | 126 | return sequelizeTypescript.transaction(async t => { |
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index 4afdbd430..9caa74e04 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { isRedundancyAccepted } from '@server/lib/redundancy' | 1 | import { isRedundancyAccepted } from '@server/lib/redundancy' |
2 | import { ActivityUpdate, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub' | 2 | import { ActivityUpdate, ActivityUpdateObject, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub' |
3 | import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' | 3 | import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' |
4 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | 4 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' |
5 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' | 5 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' |
@@ -10,16 +10,18 @@ import { sequelizeTypescript } from '../../../initializers/database' | |||
10 | import { ActorModel } from '../../../models/actor/actor' | 10 | import { ActorModel } from '../../../models/actor/actor' |
11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
12 | import { MActorFull, MActorSignature } from '../../../types/models' | 12 | import { MActorFull, MActorSignature } from '../../../types/models' |
13 | import { fetchAPObject } from '../activity' | ||
13 | import { APActorUpdater } from '../actors/updater' | 14 | import { APActorUpdater } from '../actors/updater' |
14 | import { createOrUpdateCacheFile } from '../cache-file' | 15 | import { createOrUpdateCacheFile } from '../cache-file' |
15 | import { createOrUpdateVideoPlaylist } from '../playlists' | 16 | import { createOrUpdateVideoPlaylist } from '../playlists' |
16 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | 17 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' |
17 | import { APVideoUpdater, getOrCreateAPVideo } from '../videos' | 18 | import { APVideoUpdater, getOrCreateAPVideo } from '../videos' |
18 | 19 | ||
19 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { | 20 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate<ActivityUpdateObject>>) { |
20 | const { activity, byActor } = options | 21 | const { activity, byActor } = options |
21 | 22 | ||
22 | const objectType = activity.object.type | 23 | const object = await fetchAPObject(activity.object) |
24 | const objectType = object.type | ||
23 | 25 | ||
24 | if (objectType === 'Video') { | 26 | if (objectType === 'Video') { |
25 | return retryTransactionWrapper(processUpdateVideo, activity) | 27 | return retryTransactionWrapper(processUpdateVideo, activity) |
@@ -28,17 +30,17 @@ async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate | |||
28 | if (objectType === 'Person' || objectType === 'Application' || objectType === 'Group') { | 30 | if (objectType === 'Person' || objectType === 'Application' || objectType === 'Group') { |
29 | // We need more attributes | 31 | // We need more attributes |
30 | const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url) | 32 | const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url) |
31 | return retryTransactionWrapper(processUpdateActor, byActorFull, activity) | 33 | return retryTransactionWrapper(processUpdateActor, byActorFull, object) |
32 | } | 34 | } |
33 | 35 | ||
34 | if (objectType === 'CacheFile') { | 36 | if (objectType === 'CacheFile') { |
35 | // We need more attributes | 37 | // We need more attributes |
36 | const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url) | 38 | const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url) |
37 | return retryTransactionWrapper(processUpdateCacheFile, byActorFull, activity) | 39 | return retryTransactionWrapper(processUpdateCacheFile, byActorFull, activity, object) |
38 | } | 40 | } |
39 | 41 | ||
40 | if (objectType === 'Playlist') { | 42 | if (objectType === 'Playlist') { |
41 | return retryTransactionWrapper(processUpdatePlaylist, byActor, activity) | 43 | return retryTransactionWrapper(processUpdatePlaylist, byActor, activity, object) |
42 | } | 44 | } |
43 | 45 | ||
44 | return undefined | 46 | return undefined |
@@ -52,7 +54,7 @@ export { | |||
52 | 54 | ||
53 | // --------------------------------------------------------------------------- | 55 | // --------------------------------------------------------------------------- |
54 | 56 | ||
55 | async function processUpdateVideo (activity: ActivityUpdate) { | 57 | async function processUpdateVideo (activity: ActivityUpdate<VideoObject | string>) { |
56 | const videoObject = activity.object as VideoObject | 58 | const videoObject = activity.object as VideoObject |
57 | 59 | ||
58 | if (sanitizeAndCheckVideoTorrentObject(videoObject) === false) { | 60 | if (sanitizeAndCheckVideoTorrentObject(videoObject) === false) { |
@@ -72,11 +74,13 @@ async function processUpdateVideo (activity: ActivityUpdate) { | |||
72 | return updater.update(activity.to) | 74 | return updater.update(activity.to) |
73 | } | 75 | } |
74 | 76 | ||
75 | async function processUpdateCacheFile (byActor: MActorSignature, activity: ActivityUpdate) { | 77 | async function processUpdateCacheFile ( |
78 | byActor: MActorSignature, | ||
79 | activity: ActivityUpdate<CacheFileObject | string>, | ||
80 | cacheFileObject: CacheFileObject | ||
81 | ) { | ||
76 | if (await isRedundancyAccepted(activity, byActor) !== true) return | 82 | if (await isRedundancyAccepted(activity, byActor) !== true) return |
77 | 83 | ||
78 | const cacheFileObject = activity.object as CacheFileObject | ||
79 | |||
80 | if (!isCacheFileObjectValid(cacheFileObject)) { | 84 | if (!isCacheFileObjectValid(cacheFileObject)) { |
81 | logger.debug('Cache file object sent by update is not valid.', { cacheFileObject }) | 85 | logger.debug('Cache file object sent by update is not valid.', { cacheFileObject }) |
82 | return undefined | 86 | return undefined |
@@ -96,19 +100,19 @@ async function processUpdateCacheFile (byActor: MActorSignature, activity: Activ | |||
96 | } | 100 | } |
97 | } | 101 | } |
98 | 102 | ||
99 | async function processUpdateActor (actor: MActorFull, activity: ActivityUpdate) { | 103 | async function processUpdateActor (actor: MActorFull, actorObject: ActivityPubActor) { |
100 | const actorObject = activity.object as ActivityPubActor | ||
101 | |||
102 | logger.debug('Updating remote account "%s".', actorObject.url) | 104 | logger.debug('Updating remote account "%s".', actorObject.url) |
103 | 105 | ||
104 | const updater = new APActorUpdater(actorObject, actor) | 106 | const updater = new APActorUpdater(actorObject, actor) |
105 | return updater.update() | 107 | return updater.update() |
106 | } | 108 | } |
107 | 109 | ||
108 | async function processUpdatePlaylist (byActor: MActorSignature, activity: ActivityUpdate) { | 110 | async function processUpdatePlaylist ( |
109 | const playlistObject = activity.object as PlaylistObject | 111 | byActor: MActorSignature, |
112 | activity: ActivityUpdate<PlaylistObject | string>, | ||
113 | playlistObject: PlaylistObject | ||
114 | ) { | ||
110 | const byAccount = byActor.Account | 115 | const byAccount = byActor.Account |
111 | |||
112 | if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url) | 116 | if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url) |
113 | 117 | ||
114 | await createOrUpdateVideoPlaylist(playlistObject, activity.to) | 118 | await createOrUpdateVideoPlaylist(playlistObject, activity.to) |
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 0e996ab80..2cd4db14d 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts | |||
@@ -1,6 +1,14 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { getServerActor } from '@server/models/application/application' | 2 | import { getServerActor } from '@server/models/application/application' |
3 | import { ActivityAudience, ActivityCreate, ContextType, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' | 3 | import { |
4 | ActivityAudience, | ||
5 | ActivityCreate, | ||
6 | ActivityCreateObject, | ||
7 | ContextType, | ||
8 | VideoCommentObject, | ||
9 | VideoPlaylistPrivacy, | ||
10 | VideoPrivacy | ||
11 | } from '@shared/models' | ||
4 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | 12 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
5 | import { VideoCommentModel } from '../../../models/video/video-comment' | 13 | import { VideoCommentModel } from '../../../models/video/video-comment' |
6 | import { | 14 | import { |
@@ -107,7 +115,7 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: | |||
107 | 115 | ||
108 | const byActor = comment.Account.Actor | 116 | const byActor = comment.Account.Actor |
109 | const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, transaction) | 117 | const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, transaction) |
110 | const commentObject = comment.toActivityPubObject(threadParentComments) | 118 | const commentObject = comment.toActivityPubObject(threadParentComments) as VideoCommentObject |
111 | 119 | ||
112 | const actorsInvolvedInComment = await getActorsInvolvedInVideo(comment.Video, transaction) | 120 | const actorsInvolvedInComment = await getActorsInvolvedInVideo(comment.Video, transaction) |
113 | // Add the actor that commented too | 121 | // Add the actor that commented too |
@@ -168,7 +176,12 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: | |||
168 | }) | 176 | }) |
169 | } | 177 | } |
170 | 178 | ||
171 | function buildCreateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityCreate { | 179 | function buildCreateActivity <T extends ActivityCreateObject> ( |
180 | url: string, | ||
181 | byActor: MActorLight, | ||
182 | object: T, | ||
183 | audience?: ActivityAudience | ||
184 | ): ActivityCreate<T> { | ||
172 | if (!audience) audience = getAudience(byActor) | 185 | if (!audience) audience = getAudience(byActor) |
173 | 186 | ||
174 | return audiencify( | 187 | return audiencify( |
@@ -176,7 +189,9 @@ function buildCreateActivity (url: string, byActor: MActorLight, object: any, au | |||
176 | type: 'Create' as 'Create', | 189 | type: 'Create' as 'Create', |
177 | id: url + '/activity', | 190 | id: url + '/activity', |
178 | actor: byActor.url, | 191 | actor: byActor.url, |
179 | object: audiencify(object, audience) | 192 | object: typeof object === 'string' |
193 | ? object | ||
194 | : audiencify(object, audience) | ||
180 | }, | 195 | }, |
181 | audience | 196 | audience |
182 | ) | 197 | ) |
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index b8eb47ff6..b0b48c9c4 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts | |||
@@ -1,14 +1,5 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { | 2 | import { ActivityAudience, ActivityDislike, ActivityLike, ActivityUndo, ActivityUndoObject, ContextType } from '@shared/models' |
3 | ActivityAnnounce, | ||
4 | ActivityAudience, | ||
5 | ActivityCreate, | ||
6 | ActivityDislike, | ||
7 | ActivityFollow, | ||
8 | ActivityLike, | ||
9 | ActivityUndo, | ||
10 | ContextType | ||
11 | } from '@shared/models' | ||
12 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
13 | import { VideoModel } from '../../../models/video/video' | 4 | import { VideoModel } from '../../../models/video/video' |
14 | import { | 5 | import { |
@@ -128,12 +119,12 @@ export { | |||
128 | 119 | ||
129 | // --------------------------------------------------------------------------- | 120 | // --------------------------------------------------------------------------- |
130 | 121 | ||
131 | function undoActivityData ( | 122 | function undoActivityData <T extends ActivityUndoObject> ( |
132 | url: string, | 123 | url: string, |
133 | byActor: MActorAudience, | 124 | byActor: MActorAudience, |
134 | object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, | 125 | object: T, |
135 | audience?: ActivityAudience | 126 | audience?: ActivityAudience |
136 | ): ActivityUndo { | 127 | ): ActivityUndo<T> { |
137 | if (!audience) audience = getAudience(byActor) | 128 | if (!audience) audience = getAudience(byActor) |
138 | 129 | ||
139 | return audiencify( | 130 | return audiencify( |
@@ -151,7 +142,7 @@ async function sendUndoVideoRelatedActivity (options: { | |||
151 | byActor: MActor | 142 | byActor: MActor |
152 | video: MVideoAccountLight | 143 | video: MVideoAccountLight |
153 | url: string | 144 | url: string |
154 | activity: ActivityFollow | ActivityCreate | ActivityAnnounce | 145 | activity: ActivityUndoObject |
155 | contextType: ContextType | 146 | contextType: ContextType |
156 | transaction: Transaction | 147 | transaction: Transaction |
157 | }) { | 148 | }) { |
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index 379e2d9d8..f3fb741c6 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { getServerActor } from '@server/models/application/application' | 2 | import { getServerActor } from '@server/models/application/application' |
3 | import { ActivityAudience, ActivityUpdate, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' | 3 | import { ActivityAudience, ActivityUpdate, ActivityUpdateObject, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' |
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { AccountModel } from '../../../models/account/account' | 5 | import { AccountModel } from '../../../models/account/account' |
6 | import { VideoModel } from '../../../models/video/video' | 6 | import { VideoModel } from '../../../models/video/video' |
@@ -10,8 +10,7 @@ import { | |||
10 | MActor, | 10 | MActor, |
11 | MActorLight, | 11 | MActorLight, |
12 | MChannelDefault, | 12 | MChannelDefault, |
13 | MVideoAP, | 13 | MVideoAPLight, |
14 | MVideoAPWithoutCaption, | ||
15 | MVideoPlaylistFull, | 14 | MVideoPlaylistFull, |
16 | MVideoRedundancyVideo | 15 | MVideoRedundancyVideo |
17 | } from '../../../types/models' | 16 | } from '../../../types/models' |
@@ -20,10 +19,10 @@ import { getUpdateActivityPubUrl } from '../url' | |||
20 | import { getActorsInvolvedInVideo } from './shared' | 19 | import { getActorsInvolvedInVideo } from './shared' |
21 | import { broadcastToFollowers, sendVideoRelatedActivity } from './shared/send-utils' | 20 | import { broadcastToFollowers, sendVideoRelatedActivity } from './shared/send-utils' |
22 | 21 | ||
23 | async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, transaction: Transaction, overriddenByActor?: MActor) { | 22 | async function sendUpdateVideo (videoArg: MVideoAPLight, transaction: Transaction, overriddenByActor?: MActor) { |
24 | const video = videoArg as MVideoAP | 23 | if (!videoArg.hasPrivacyForFederation()) return undefined |
25 | 24 | ||
26 | if (!video.hasPrivacyForFederation()) return undefined | 25 | const video = await videoArg.lightAPToFullAP(transaction) |
27 | 26 | ||
28 | logger.info('Creating job to update video %s.', video.url) | 27 | logger.info('Creating job to update video %s.', video.url) |
29 | 28 | ||
@@ -31,11 +30,6 @@ async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, transaction: T | |||
31 | 30 | ||
32 | const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) | 31 | const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) |
33 | 32 | ||
34 | // Needed to build the AP object | ||
35 | if (!video.VideoCaptions) { | ||
36 | video.VideoCaptions = await video.$get('VideoCaptions', { transaction }) | ||
37 | } | ||
38 | |||
39 | const videoObject = await video.toActivityPubObject() | 33 | const videoObject = await video.toActivityPubObject() |
40 | const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC) | 34 | const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC) |
41 | 35 | ||
@@ -143,7 +137,12 @@ export { | |||
143 | 137 | ||
144 | // --------------------------------------------------------------------------- | 138 | // --------------------------------------------------------------------------- |
145 | 139 | ||
146 | function buildUpdateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityUpdate { | 140 | function buildUpdateActivity ( |
141 | url: string, | ||
142 | byActor: MActorLight, | ||
143 | object: ActivityUpdateObject, | ||
144 | audience?: ActivityAudience | ||
145 | ): ActivityUpdate<ActivityUpdateObject> { | ||
147 | if (!audience) audience = getAudience(byActor) | 146 | if (!audience) audience = getAudience(byActor) |
148 | 147 | ||
149 | return audiencify( | 148 | return audiencify( |
diff --git a/server/lib/activitypub/videos/federate.ts b/server/lib/activitypub/videos/federate.ts index bd0c54b0c..d7e251153 100644 --- a/server/lib/activitypub/videos/federate.ts +++ b/server/lib/activitypub/videos/federate.ts | |||
@@ -1,10 +1,9 @@ | |||
1 | import { Transaction } from 'sequelize/types' | 1 | import { Transaction } from 'sequelize/types' |
2 | import { isArray } from '@server/helpers/custom-validators/misc' | 2 | import { MVideoAP, MVideoAPLight } from '@server/types/models' |
3 | import { MVideoAP, MVideoAPWithoutCaption } from '@server/types/models' | ||
4 | import { sendCreateVideo, sendUpdateVideo } from '../send' | 3 | import { sendCreateVideo, sendUpdateVideo } from '../send' |
5 | import { shareVideoByServerAndChannel } from '../share' | 4 | import { shareVideoByServerAndChannel } from '../share' |
6 | 5 | ||
7 | async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: Transaction) { | 6 | async function federateVideoIfNeeded (videoArg: MVideoAPLight, isNewVideo: boolean, transaction?: Transaction) { |
8 | const video = videoArg as MVideoAP | 7 | const video = videoArg as MVideoAP |
9 | 8 | ||
10 | if ( | 9 | if ( |
@@ -13,13 +12,7 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid | |||
13 | // Check the video is public/unlisted and published | 12 | // Check the video is public/unlisted and published |
14 | video.hasPrivacyForFederation() && video.hasStateForFederation() | 13 | video.hasPrivacyForFederation() && video.hasStateForFederation() |
15 | ) { | 14 | ) { |
16 | // Fetch more attributes that we will need to serialize in AP object | 15 | const video = await videoArg.lightAPToFullAP(transaction) |
17 | if (isArray(video.VideoCaptions) === false) { | ||
18 | video.VideoCaptions = await video.$get('VideoCaptions', { | ||
19 | attributes: [ 'filename', 'language' ], | ||
20 | transaction | ||
21 | }) | ||
22 | } | ||
23 | 16 | ||
24 | if (isNewVideo) { | 17 | if (isNewVideo) { |
25 | // Now we'll add the video's meta data to our followers | 18 | // Now we'll add the video's meta data to our followers |
diff --git a/server/lib/activitypub/videos/get.ts b/server/lib/activitypub/videos/get.ts index 14ba55034..92387c5d4 100644 --- a/server/lib/activitypub/videos/get.ts +++ b/server/lib/activitypub/videos/get.ts | |||
@@ -3,7 +3,7 @@ import { logger } from '@server/helpers/logger' | |||
3 | import { JobQueue } from '@server/lib/job-queue' | 3 | import { JobQueue } from '@server/lib/job-queue' |
4 | import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders' | 4 | import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders' |
5 | import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models' | 5 | import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models' |
6 | import { APObject } from '@shared/models' | 6 | import { APObjectId } from '@shared/models' |
7 | import { getAPId } from '../activity' | 7 | import { getAPId } from '../activity' |
8 | import { refreshVideoIfNeeded } from './refresh' | 8 | import { refreshVideoIfNeeded } from './refresh' |
9 | import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared' | 9 | import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared' |
@@ -15,21 +15,21 @@ type GetVideoResult <T> = Promise<{ | |||
15 | }> | 15 | }> |
16 | 16 | ||
17 | type GetVideoParamAll = { | 17 | type GetVideoParamAll = { |
18 | videoObject: APObject | 18 | videoObject: APObjectId |
19 | syncParam?: SyncParam | 19 | syncParam?: SyncParam |
20 | fetchType?: 'all' | 20 | fetchType?: 'all' |
21 | allowRefresh?: boolean | 21 | allowRefresh?: boolean |
22 | } | 22 | } |
23 | 23 | ||
24 | type GetVideoParamImmutable = { | 24 | type GetVideoParamImmutable = { |
25 | videoObject: APObject | 25 | videoObject: APObjectId |
26 | syncParam?: SyncParam | 26 | syncParam?: SyncParam |
27 | fetchType: 'only-immutable-attributes' | 27 | fetchType: 'only-immutable-attributes' |
28 | allowRefresh: false | 28 | allowRefresh: false |
29 | } | 29 | } |
30 | 30 | ||
31 | type GetVideoParamOther = { | 31 | type GetVideoParamOther = { |
32 | videoObject: APObject | 32 | videoObject: APObjectId |
33 | syncParam?: SyncParam | 33 | syncParam?: SyncParam |
34 | fetchType?: 'all' | 'only-video' | 34 | fetchType?: 'all' | 'only-video' |
35 | allowRefresh?: boolean | 35 | allowRefresh?: boolean |
diff --git a/server/lib/activitypub/videos/shared/abstract-builder.ts b/server/lib/activitypub/videos/shared/abstract-builder.ts index c0b92c93d..98c2f58eb 100644 --- a/server/lib/activitypub/videos/shared/abstract-builder.ts +++ b/server/lib/activitypub/videos/shared/abstract-builder.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { CreationAttributes, Transaction } from 'sequelize/types' | 1 | import { CreationAttributes, Transaction } from 'sequelize/types' |
2 | import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils' | 2 | import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils' |
3 | import { logger, LoggerTagsFn } from '@server/helpers/logger' | 3 | import { logger, LoggerTagsFn } from '@server/helpers/logger' |
4 | import { updatePlaceholderThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail' | 4 | import { updateRemoteVideoThumbnail } from '@server/lib/thumbnail' |
5 | import { setVideoTags } from '@server/lib/video' | 5 | import { setVideoTags } from '@server/lib/video' |
6 | import { StoryboardModel } from '@server/models/video/storyboard' | ||
6 | import { VideoCaptionModel } from '@server/models/video/video-caption' | 7 | import { VideoCaptionModel } from '@server/models/video/video-caption' |
7 | import { VideoFileModel } from '@server/models/video/video-file' | 8 | import { VideoFileModel } from '@server/models/video/video-file' |
8 | import { VideoLiveModel } from '@server/models/video/video-live' | 9 | import { VideoLiveModel } from '@server/models/video/video-live' |
@@ -10,20 +11,19 @@ import { VideoStreamingPlaylistModel } from '@server/models/video/video-streamin | |||
10 | import { | 11 | import { |
11 | MStreamingPlaylistFiles, | 12 | MStreamingPlaylistFiles, |
12 | MStreamingPlaylistFilesVideo, | 13 | MStreamingPlaylistFilesVideo, |
13 | MThumbnail, | ||
14 | MVideoCaption, | 14 | MVideoCaption, |
15 | MVideoFile, | 15 | MVideoFile, |
16 | MVideoFullLight, | 16 | MVideoFullLight, |
17 | MVideoThumbnail | 17 | MVideoThumbnail |
18 | } from '@server/types/models' | 18 | } from '@server/types/models' |
19 | import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models' | 19 | import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models' |
20 | import { getOrCreateAPActor } from '../../actors' | 20 | import { findOwner, getOrCreateAPActor } from '../../actors' |
21 | import { checkUrlsSameHost } from '../../url' | ||
22 | import { | 21 | import { |
23 | getCaptionAttributesFromObject, | 22 | getCaptionAttributesFromObject, |
24 | getFileAttributesFromUrl, | 23 | getFileAttributesFromUrl, |
25 | getLiveAttributesFromObject, | 24 | getLiveAttributesFromObject, |
26 | getPreviewFromIcons, | 25 | getPreviewFromIcons, |
26 | getStoryboardAttributeFromObject, | ||
27 | getStreamingPlaylistAttributesFromObject, | 27 | getStreamingPlaylistAttributesFromObject, |
28 | getTagsFromObject, | 28 | getTagsFromObject, |
29 | getThumbnailFromIcons | 29 | getThumbnailFromIcons |
@@ -35,38 +35,40 @@ export abstract class APVideoAbstractBuilder { | |||
35 | protected abstract lTags: LoggerTagsFn | 35 | protected abstract lTags: LoggerTagsFn |
36 | 36 | ||
37 | protected async getOrCreateVideoChannelFromVideoObject () { | 37 | protected async getOrCreateVideoChannelFromVideoObject () { |
38 | const channel = this.videoObject.attributedTo.find(a => a.type === 'Group') | 38 | const channel = await findOwner(this.videoObject.id, this.videoObject.attributedTo, 'Group') |
39 | if (!channel) throw new Error('Cannot find associated video channel to video ' + this.videoObject.url) | 39 | if (!channel) throw new Error('Cannot find associated video channel to video ' + this.videoObject.url) |
40 | 40 | ||
41 | if (checkUrlsSameHost(channel.id, this.videoObject.id) !== true) { | ||
42 | throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${this.videoObject.id}`) | ||
43 | } | ||
44 | |||
45 | return getOrCreateAPActor(channel.id, 'all') | 41 | return getOrCreateAPActor(channel.id, 'all') |
46 | } | 42 | } |
47 | 43 | ||
48 | protected tryToGenerateThumbnail (video: MVideoThumbnail): Promise<MThumbnail> { | 44 | protected async setThumbnail (video: MVideoThumbnail, t?: Transaction) { |
49 | return updateVideoMiniatureFromUrl({ | 45 | const miniatureIcon = getThumbnailFromIcons(this.videoObject) |
50 | downloadUrl: getThumbnailFromIcons(this.videoObject).url, | 46 | if (!miniatureIcon) { |
51 | video, | 47 | logger.warn('Cannot find thumbnail in video object', { object: this.videoObject }) |
52 | type: ThumbnailType.MINIATURE | ||
53 | }).catch(err => { | ||
54 | logger.warn('Cannot generate thumbnail of %s.', this.videoObject.id, { err, ...this.lTags() }) | ||
55 | |||
56 | return undefined | 48 | return undefined |
49 | } | ||
50 | |||
51 | const miniatureModel = updateRemoteVideoThumbnail({ | ||
52 | fileUrl: miniatureIcon.url, | ||
53 | video, | ||
54 | type: ThumbnailType.MINIATURE, | ||
55 | size: miniatureIcon, | ||
56 | onDisk: false // Lazy download remote thumbnails | ||
57 | }) | 57 | }) |
58 | |||
59 | await video.addAndSaveThumbnail(miniatureModel, t) | ||
58 | } | 60 | } |
59 | 61 | ||
60 | protected async setPreview (video: MVideoFullLight, t?: Transaction) { | 62 | protected async setPreview (video: MVideoFullLight, t?: Transaction) { |
61 | // Don't fetch the preview that could be big, create a placeholder instead | ||
62 | const previewIcon = getPreviewFromIcons(this.videoObject) | 63 | const previewIcon = getPreviewFromIcons(this.videoObject) |
63 | if (!previewIcon) return | 64 | if (!previewIcon) return |
64 | 65 | ||
65 | const previewModel = updatePlaceholderThumbnail({ | 66 | const previewModel = updateRemoteVideoThumbnail({ |
66 | fileUrl: previewIcon.url, | 67 | fileUrl: previewIcon.url, |
67 | video, | 68 | video, |
68 | type: ThumbnailType.PREVIEW, | 69 | type: ThumbnailType.PREVIEW, |
69 | size: previewIcon | 70 | size: previewIcon, |
71 | onDisk: false // Lazy download remote previews | ||
70 | }) | 72 | }) |
71 | 73 | ||
72 | await video.addAndSaveThumbnail(previewModel, t) | 74 | await video.addAndSaveThumbnail(previewModel, t) |
@@ -107,6 +109,16 @@ export abstract class APVideoAbstractBuilder { | |||
107 | } | 109 | } |
108 | } | 110 | } |
109 | 111 | ||
112 | protected async insertOrReplaceStoryboard (video: MVideoFullLight, t: Transaction) { | ||
113 | const existingStoryboard = await StoryboardModel.loadByVideo(video.id, t) | ||
114 | if (existingStoryboard) await existingStoryboard.destroy({ transaction: t }) | ||
115 | |||
116 | const storyboardAttributes = getStoryboardAttributeFromObject(video, this.videoObject) | ||
117 | if (!storyboardAttributes) return | ||
118 | |||
119 | return StoryboardModel.create(storyboardAttributes, { transaction: t }) | ||
120 | } | ||
121 | |||
110 | protected async insertOrReplaceLive (video: MVideoFullLight, transaction: Transaction) { | 122 | protected async insertOrReplaceLive (video: MVideoFullLight, transaction: Transaction) { |
111 | const attributes = getLiveAttributesFromObject(video, this.videoObject) | 123 | const attributes = getLiveAttributesFromObject(video, this.videoObject) |
112 | const [ videoLive ] = await VideoLiveModel.upsert(attributes, { transaction, returning: true }) | 124 | const [ videoLive ] = await VideoLiveModel.upsert(attributes, { transaction, returning: true }) |
@@ -114,7 +126,7 @@ export abstract class APVideoAbstractBuilder { | |||
114 | video.VideoLive = videoLive | 126 | video.VideoLive = videoLive |
115 | } | 127 | } |
116 | 128 | ||
117 | protected async setWebTorrentFiles (video: MVideoFullLight, t: Transaction) { | 129 | protected async setWebVideoFiles (video: MVideoFullLight, t: Transaction) { |
118 | const videoFileAttributes = getFileAttributesFromUrl(video, this.videoObject.url) | 130 | const videoFileAttributes = getFileAttributesFromUrl(video, this.videoObject.url) |
119 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) | 131 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) |
120 | 132 | ||
diff --git a/server/lib/activitypub/videos/shared/creator.ts b/server/lib/activitypub/videos/shared/creator.ts index 77321d8a5..bc139e4fa 100644 --- a/server/lib/activitypub/videos/shared/creator.ts +++ b/server/lib/activitypub/videos/shared/creator.ts | |||
@@ -4,7 +4,7 @@ import { sequelizeTypescript } from '@server/initializers/database' | |||
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' | 5 | import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' |
6 | import { VideoModel } from '@server/models/video/video' | 6 | import { VideoModel } from '@server/models/video/video' |
7 | import { MThumbnail, MVideoFullLight, MVideoThumbnail } from '@server/types/models' | 7 | import { MVideoFullLight, MVideoThumbnail } from '@server/types/models' |
8 | import { VideoObject } from '@shared/models' | 8 | import { VideoObject } from '@shared/models' |
9 | import { APVideoAbstractBuilder } from './abstract-builder' | 9 | import { APVideoAbstractBuilder } from './abstract-builder' |
10 | import { getVideoAttributesFromObject } from './object-to-model-attributes' | 10 | import { getVideoAttributesFromObject } from './object-to-model-attributes' |
@@ -27,64 +27,38 @@ export class APVideoCreator extends APVideoAbstractBuilder { | |||
27 | const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to) | 27 | const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to) |
28 | const video = VideoModel.build({ ...videoData, likes: 0, dislikes: 0 }) as MVideoThumbnail | 28 | const video = VideoModel.build({ ...videoData, likes: 0, dislikes: 0 }) as MVideoThumbnail |
29 | 29 | ||
30 | const promiseThumbnail = this.tryToGenerateThumbnail(video) | ||
31 | |||
32 | let thumbnailModel: MThumbnail | ||
33 | if (waitThumbnail === true) { | ||
34 | thumbnailModel = await promiseThumbnail | ||
35 | } | ||
36 | |||
37 | const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => { | 30 | const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => { |
38 | try { | 31 | const videoCreated = await video.save({ transaction: t }) as MVideoFullLight |
39 | const videoCreated = await video.save({ transaction: t }) as MVideoFullLight | 32 | videoCreated.VideoChannel = channel |
40 | videoCreated.VideoChannel = channel | 33 | |
41 | 34 | await this.setThumbnail(videoCreated, t) | |
42 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | 35 | await this.setPreview(videoCreated, t) |
43 | 36 | await this.setWebVideoFiles(videoCreated, t) | |
44 | await this.setPreview(videoCreated, t) | 37 | await this.setStreamingPlaylists(videoCreated, t) |
45 | await this.setWebTorrentFiles(videoCreated, t) | 38 | await this.setTags(videoCreated, t) |
46 | await this.setStreamingPlaylists(videoCreated, t) | 39 | await this.setTrackers(videoCreated, t) |
47 | await this.setTags(videoCreated, t) | 40 | await this.insertOrReplaceCaptions(videoCreated, t) |
48 | await this.setTrackers(videoCreated, t) | 41 | await this.insertOrReplaceLive(videoCreated, t) |
49 | await this.insertOrReplaceCaptions(videoCreated, t) | 42 | await this.insertOrReplaceStoryboard(videoCreated, t) |
50 | await this.insertOrReplaceLive(videoCreated, t) | 43 | |
51 | 44 | // We added a video in this channel, set it as updated | |
52 | // We added a video in this channel, set it as updated | 45 | await channel.setAsUpdated(t) |
53 | await channel.setAsUpdated(t) | 46 | |
54 | 47 | const autoBlacklisted = await autoBlacklistVideoIfNeeded({ | |
55 | const autoBlacklisted = await autoBlacklistVideoIfNeeded({ | 48 | video: videoCreated, |
56 | video: videoCreated, | 49 | user: undefined, |
57 | user: undefined, | 50 | isRemote: true, |
58 | isRemote: true, | 51 | isNew: true, |
59 | isNew: true, | 52 | transaction: t |
60 | transaction: t | 53 | }) |
61 | }) | ||
62 | |||
63 | logger.info('Remote video with uuid %s inserted.', this.videoObject.uuid, this.lTags()) | ||
64 | 54 | ||
65 | Hooks.runAction('action:activity-pub.remote-video.created', { video: videoCreated, videoAPObject: this.videoObject }) | 55 | logger.info('Remote video with uuid %s inserted.', this.videoObject.uuid, this.lTags()) |
66 | 56 | ||
67 | return { autoBlacklisted, videoCreated } | 57 | Hooks.runAction('action:activity-pub.remote-video.created', { video: videoCreated, videoAPObject: this.videoObject }) |
68 | } catch (err) { | ||
69 | // FIXME: Use rollback hook when https://github.com/sequelize/sequelize/pull/13038 is released | ||
70 | if (thumbnailModel) await thumbnailModel.removeThumbnail() | ||
71 | 58 | ||
72 | throw err | 59 | return { autoBlacklisted, videoCreated } |
73 | } | ||
74 | }) | 60 | }) |
75 | 61 | ||
76 | if (waitThumbnail === false) { | ||
77 | // Error is already caught above | ||
78 | // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||
79 | promiseThumbnail.then(thumbnailModel => { | ||
80 | if (!thumbnailModel) return | ||
81 | |||
82 | thumbnailModel = videoCreated.id | ||
83 | |||
84 | return thumbnailModel.save() | ||
85 | }) | ||
86 | } | ||
87 | |||
88 | return { autoBlacklisted, videoCreated } | 62 | return { autoBlacklisted, videoCreated } |
89 | } | 63 | } |
90 | } | 64 | } |
diff --git a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts index 8fd0a816c..a9e0bed97 100644 --- a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts +++ b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { maxBy, minBy } from 'lodash' | 1 | import { maxBy, minBy } from 'lodash' |
2 | import { decode as magnetUriDecode } from 'magnet-uri' | 2 | import { decode as magnetUriDecode } from 'magnet-uri' |
3 | import { basename } from 'path' | 3 | import { basename, extname } from 'path' |
4 | import { isAPVideoFileUrlMetadataObject } from '@server/helpers/custom-validators/activitypub/videos' | 4 | import { isAPVideoFileUrlMetadataObject } from '@server/helpers/custom-validators/activitypub/videos' |
5 | import { isVideoFileInfoHashValid } from '@server/helpers/custom-validators/videos' | 5 | import { isVideoFileInfoHashValid } from '@server/helpers/custom-validators/videos' |
6 | import { logger } from '@server/helpers/logger' | 6 | import { logger } from '@server/helpers/logger' |
@@ -25,6 +25,9 @@ import { | |||
25 | VideoStreamingPlaylistType | 25 | VideoStreamingPlaylistType |
26 | } from '@shared/models' | 26 | } from '@shared/models' |
27 | import { getDurationFromActivityStream } from '../../activity' | 27 | import { getDurationFromActivityStream } from '../../activity' |
28 | import { isArray } from '@server/helpers/custom-validators/misc' | ||
29 | import { generateImageFilename } from '@server/helpers/image-utils' | ||
30 | import { arrayify } from '@shared/core-utils' | ||
28 | 31 | ||
29 | function getThumbnailFromIcons (videoObject: VideoObject) { | 32 | function getThumbnailFromIcons (videoObject: VideoObject) { |
30 | let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth) | 33 | let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth) |
@@ -166,6 +169,26 @@ function getCaptionAttributesFromObject (video: MVideoId, videoObject: VideoObje | |||
166 | })) | 169 | })) |
167 | } | 170 | } |
168 | 171 | ||
172 | function getStoryboardAttributeFromObject (video: MVideoId, videoObject: VideoObject) { | ||
173 | if (!isArray(videoObject.preview)) return undefined | ||
174 | |||
175 | const storyboard = videoObject.preview.find(p => p.rel.includes('storyboard')) | ||
176 | if (!storyboard) return undefined | ||
177 | |||
178 | const url = arrayify(storyboard.url).find(u => u.mediaType === 'image/jpeg') | ||
179 | |||
180 | return { | ||
181 | filename: generateImageFilename(extname(url.href)), | ||
182 | totalHeight: url.height, | ||
183 | totalWidth: url.width, | ||
184 | spriteHeight: url.tileHeight, | ||
185 | spriteWidth: url.tileWidth, | ||
186 | spriteDuration: getDurationFromActivityStream(url.tileDuration), | ||
187 | fileUrl: url.href, | ||
188 | videoId: video.id | ||
189 | } | ||
190 | } | ||
191 | |||
169 | function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) { | 192 | function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) { |
170 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) | 193 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) |
171 | ? VideoPrivacy.PUBLIC | 194 | ? VideoPrivacy.PUBLIC |
@@ -228,6 +251,7 @@ export { | |||
228 | 251 | ||
229 | getLiveAttributesFromObject, | 252 | getLiveAttributesFromObject, |
230 | getCaptionAttributesFromObject, | 253 | getCaptionAttributesFromObject, |
254 | getStoryboardAttributeFromObject, | ||
231 | 255 | ||
232 | getVideoAttributesFromObject | 256 | getVideoAttributesFromObject |
233 | } | 257 | } |
diff --git a/server/lib/activitypub/videos/updater.ts b/server/lib/activitypub/videos/updater.ts index 6ddd2301b..522d7b043 100644 --- a/server/lib/activitypub/videos/updater.ts +++ b/server/lib/activitypub/videos/updater.ts | |||
@@ -41,7 +41,7 @@ export class APVideoUpdater extends APVideoAbstractBuilder { | |||
41 | try { | 41 | try { |
42 | const channelActor = await this.getOrCreateVideoChannelFromVideoObject() | 42 | const channelActor = await this.getOrCreateVideoChannelFromVideoObject() |
43 | 43 | ||
44 | const thumbnailModel = await this.tryToGenerateThumbnail(this.video) | 44 | const thumbnailModel = await this.setThumbnail(this.video) |
45 | 45 | ||
46 | this.checkChannelUpdateOrThrow(channelActor) | 46 | this.checkChannelUpdateOrThrow(channelActor) |
47 | 47 | ||
@@ -50,15 +50,21 @@ export class APVideoUpdater extends APVideoAbstractBuilder { | |||
50 | if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel) | 50 | if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel) |
51 | 51 | ||
52 | await runInReadCommittedTransaction(async t => { | 52 | await runInReadCommittedTransaction(async t => { |
53 | await this.setWebTorrentFiles(videoUpdated, t) | 53 | await this.setWebVideoFiles(videoUpdated, t) |
54 | await this.setStreamingPlaylists(videoUpdated, t) | 54 | await this.setStreamingPlaylists(videoUpdated, t) |
55 | }) | 55 | }) |
56 | 56 | ||
57 | await Promise.all([ | 57 | await Promise.all([ |
58 | runInReadCommittedTransaction(t => this.setTags(videoUpdated, t)), | 58 | runInReadCommittedTransaction(t => this.setTags(videoUpdated, t)), |
59 | runInReadCommittedTransaction(t => this.setTrackers(videoUpdated, t)), | 59 | runInReadCommittedTransaction(t => this.setTrackers(videoUpdated, t)), |
60 | this.setOrDeleteLive(videoUpdated), | 60 | runInReadCommittedTransaction(t => this.setStoryboard(videoUpdated, t)), |
61 | this.setPreview(videoUpdated) | 61 | runInReadCommittedTransaction(t => { |
62 | return Promise.all([ | ||
63 | this.setPreview(videoUpdated, t), | ||
64 | this.setThumbnail(videoUpdated, t) | ||
65 | ]) | ||
66 | }), | ||
67 | this.setOrDeleteLive(videoUpdated) | ||
62 | ]) | 68 | ]) |
63 | 69 | ||
64 | await runInReadCommittedTransaction(t => this.setCaptions(videoUpdated, t)) | 70 | await runInReadCommittedTransaction(t => this.setCaptions(videoUpdated, t)) |
@@ -138,6 +144,10 @@ export class APVideoUpdater extends APVideoAbstractBuilder { | |||
138 | await this.insertOrReplaceCaptions(videoUpdated, t) | 144 | await this.insertOrReplaceCaptions(videoUpdated, t) |
139 | } | 145 | } |
140 | 146 | ||
147 | private async setStoryboard (videoUpdated: MVideoFullLight, t: Transaction) { | ||
148 | await this.insertOrReplaceStoryboard(videoUpdated, t) | ||
149 | } | ||
150 | |||
141 | private async setOrDeleteLive (videoUpdated: MVideoFullLight, transaction?: Transaction) { | 151 | private async setOrDeleteLive (videoUpdated: MVideoFullLight, transaction?: Transaction) { |
142 | if (!this.video.isLive) return | 152 | if (!this.video.isLive) return |
143 | 153 | ||
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 18b16bee1..be6df1792 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -32,6 +32,7 @@ import { getActivityStreamDuration } from './activitypub/activity' | |||
32 | import { getBiggestActorImage } from './actor-image' | 32 | import { getBiggestActorImage } from './actor-image' |
33 | import { Hooks } from './plugins/hooks' | 33 | import { Hooks } from './plugins/hooks' |
34 | import { ServerConfigManager } from './server-config-manager' | 34 | import { ServerConfigManager } from './server-config-manager' |
35 | import { isVideoInPrivateDirectory } from './video-privacy' | ||
35 | 36 | ||
36 | type Tags = { | 37 | type Tags = { |
37 | ogType: string | 38 | ogType: string |
@@ -106,7 +107,7 @@ class ClientHtml { | |||
106 | ]) | 107 | ]) |
107 | 108 | ||
108 | // Let Angular application handle errors | 109 | // Let Angular application handle errors |
109 | if (!video || video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL || video.VideoBlacklist) { | 110 | if (!video || isVideoInPrivateDirectory(video.privacy) || video.VideoBlacklist) { |
110 | res.status(HttpStatusCode.NOT_FOUND_404) | 111 | res.status(HttpStatusCode.NOT_FOUND_404) |
111 | return html | 112 | return html |
112 | } | 113 | } |
diff --git a/server/lib/files-cache/avatar-permanent-file-cache.ts b/server/lib/files-cache/avatar-permanent-file-cache.ts new file mode 100644 index 000000000..0c508b063 --- /dev/null +++ b/server/lib/files-cache/avatar-permanent-file-cache.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import { CONFIG } from '@server/initializers/config' | ||
2 | import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' | ||
3 | import { ActorImageModel } from '@server/models/actor/actor-image' | ||
4 | import { MActorImage } from '@server/types/models' | ||
5 | import { AbstractPermanentFileCache } from './shared' | ||
6 | |||
7 | export class AvatarPermanentFileCache extends AbstractPermanentFileCache<MActorImage> { | ||
8 | |||
9 | constructor () { | ||
10 | super(CONFIG.STORAGE.ACTOR_IMAGES_DIR) | ||
11 | } | ||
12 | |||
13 | protected loadModel (filename: string) { | ||
14 | return ActorImageModel.loadByName(filename) | ||
15 | } | ||
16 | |||
17 | protected getImageSize (image: MActorImage): { width: number, height: number } { | ||
18 | if (image.width && image.height) { | ||
19 | return { | ||
20 | height: image.height, | ||
21 | width: image.width | ||
22 | } | ||
23 | } | ||
24 | |||
25 | return ACTOR_IMAGES_SIZE[image.type][0] | ||
26 | } | ||
27 | } | ||
diff --git a/server/lib/files-cache/index.ts b/server/lib/files-cache/index.ts index e5853f7d6..5630a9b80 100644 --- a/server/lib/files-cache/index.ts +++ b/server/lib/files-cache/index.ts | |||
@@ -1,3 +1,6 @@ | |||
1 | export * from './videos-preview-cache' | 1 | export * from './avatar-permanent-file-cache' |
2 | export * from './videos-caption-cache' | 2 | export * from './video-miniature-permanent-file-cache' |
3 | export * from './videos-torrent-cache' | 3 | export * from './video-captions-simple-file-cache' |
4 | export * from './video-previews-simple-file-cache' | ||
5 | export * from './video-storyboards-simple-file-cache' | ||
6 | export * from './video-torrents-simple-file-cache' | ||
diff --git a/server/lib/files-cache/shared/abstract-permanent-file-cache.ts b/server/lib/files-cache/shared/abstract-permanent-file-cache.ts new file mode 100644 index 000000000..f990e9872 --- /dev/null +++ b/server/lib/files-cache/shared/abstract-permanent-file-cache.ts | |||
@@ -0,0 +1,132 @@ | |||
1 | import express from 'express' | ||
2 | import { LRUCache } from 'lru-cache' | ||
3 | import { Model } from 'sequelize' | ||
4 | import { logger } from '@server/helpers/logger' | ||
5 | import { CachePromise } from '@server/helpers/promise-cache' | ||
6 | import { LRU_CACHE, STATIC_MAX_AGE } from '@server/initializers/constants' | ||
7 | import { downloadImageFromWorker } from '@server/lib/worker/parent-process' | ||
8 | import { HttpStatusCode } from '@shared/models' | ||
9 | |||
10 | type ImageModel = { | ||
11 | fileUrl: string | ||
12 | filename: string | ||
13 | onDisk: boolean | ||
14 | |||
15 | isOwned (): boolean | ||
16 | getPath (): string | ||
17 | |||
18 | save (): Promise<Model> | ||
19 | } | ||
20 | |||
21 | export abstract class AbstractPermanentFileCache <M extends ImageModel> { | ||
22 | // Unsafe because it can return paths that do not exist anymore | ||
23 | private readonly filenameToPathUnsafeCache = new LRUCache<string, string>({ | ||
24 | max: LRU_CACHE.FILENAME_TO_PATH_PERMANENT_FILE_CACHE.MAX_SIZE | ||
25 | }) | ||
26 | |||
27 | protected abstract getImageSize (image: M): { width: number, height: number } | ||
28 | protected abstract loadModel (filename: string): Promise<M> | ||
29 | |||
30 | constructor (private readonly directory: string) { | ||
31 | |||
32 | } | ||
33 | |||
34 | async lazyServe (options: { | ||
35 | filename: string | ||
36 | res: express.Response | ||
37 | next: express.NextFunction | ||
38 | }) { | ||
39 | const { filename, res, next } = options | ||
40 | |||
41 | if (this.filenameToPathUnsafeCache.has(filename)) { | ||
42 | return res.sendFile(this.filenameToPathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER }) | ||
43 | } | ||
44 | |||
45 | const image = await this.lazyLoadIfNeeded(filename) | ||
46 | if (!image) return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
47 | |||
48 | const path = image.getPath() | ||
49 | this.filenameToPathUnsafeCache.set(filename, path) | ||
50 | |||
51 | return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }, (err: any) => { | ||
52 | if (!err) return | ||
53 | |||
54 | this.onServeError({ err, image, next, filename }) | ||
55 | }) | ||
56 | } | ||
57 | |||
58 | @CachePromise({ | ||
59 | keyBuilder: filename => filename | ||
60 | }) | ||
61 | private async lazyLoadIfNeeded (filename: string) { | ||
62 | const image = await this.loadModel(filename) | ||
63 | if (!image) return undefined | ||
64 | |||
65 | if (image.onDisk === false) { | ||
66 | if (!image.fileUrl) return undefined | ||
67 | |||
68 | try { | ||
69 | await this.downloadRemoteFile(image) | ||
70 | } catch (err) { | ||
71 | logger.warn('Cannot process remote image %s.', image.fileUrl, { err }) | ||
72 | |||
73 | return undefined | ||
74 | } | ||
75 | } | ||
76 | |||
77 | return image | ||
78 | } | ||
79 | |||
80 | async downloadRemoteFile (image: M) { | ||
81 | logger.info('Download remote image %s lazily.', image.fileUrl) | ||
82 | |||
83 | const destination = await this.downloadImage({ | ||
84 | filename: image.filename, | ||
85 | fileUrl: image.fileUrl, | ||
86 | size: this.getImageSize(image) | ||
87 | }) | ||
88 | |||
89 | image.onDisk = true | ||
90 | image.save() | ||
91 | .catch(err => logger.error('Cannot save new image disk state.', { err })) | ||
92 | |||
93 | return destination | ||
94 | } | ||
95 | |||
96 | private onServeError (options: { | ||
97 | err: any | ||
98 | image: M | ||
99 | filename: string | ||
100 | next: express.NextFunction | ||
101 | }) { | ||
102 | const { err, image, filename, next } = options | ||
103 | |||
104 | // It seems this actor image is not on the disk anymore | ||
105 | if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) { | ||
106 | logger.error('Cannot lazy serve image %s.', filename, { err }) | ||
107 | |||
108 | this.filenameToPathUnsafeCache.delete(filename) | ||
109 | |||
110 | image.onDisk = false | ||
111 | image.save() | ||
112 | .catch(err => logger.error('Cannot save new image disk state.', { err })) | ||
113 | } | ||
114 | |||
115 | return next(err) | ||
116 | } | ||
117 | |||
118 | private downloadImage (options: { | ||
119 | fileUrl: string | ||
120 | filename: string | ||
121 | size: { width: number, height: number } | ||
122 | }) { | ||
123 | const downloaderOptions = { | ||
124 | url: options.fileUrl, | ||
125 | destDir: this.directory, | ||
126 | destName: options.filename, | ||
127 | size: options.size | ||
128 | } | ||
129 | |||
130 | return downloadImageFromWorker(downloaderOptions) | ||
131 | } | ||
132 | } | ||
diff --git a/server/lib/files-cache/abstract-video-static-file-cache.ts b/server/lib/files-cache/shared/abstract-simple-file-cache.ts index a7ac88525..6fab322cd 100644 --- a/server/lib/files-cache/abstract-video-static-file-cache.ts +++ b/server/lib/files-cache/shared/abstract-simple-file-cache.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | import { remove } from 'fs-extra' | 1 | import { remove } from 'fs-extra' |
2 | import { logger } from '../../helpers/logger' | 2 | import { logger } from '../../../helpers/logger' |
3 | import memoizee from 'memoizee' | 3 | import memoizee from 'memoizee' |
4 | 4 | ||
5 | type GetFilePathResult = { isOwned: boolean, path: string, downloadName?: string } | undefined | 5 | type GetFilePathResult = { isOwned: boolean, path: string, downloadName?: string } | undefined |
6 | 6 | ||
7 | export abstract class AbstractVideoStaticFileCache <T> { | 7 | export abstract class AbstractSimpleFileCache <T> { |
8 | 8 | ||
9 | getFilePath: (params: T) => Promise<GetFilePathResult> | 9 | getFilePath: (params: T) => Promise<GetFilePathResult> |
10 | 10 | ||
diff --git a/server/lib/files-cache/shared/index.ts b/server/lib/files-cache/shared/index.ts new file mode 100644 index 000000000..61c4aacc7 --- /dev/null +++ b/server/lib/files-cache/shared/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './abstract-permanent-file-cache' | ||
2 | export * from './abstract-simple-file-cache' | ||
diff --git a/server/lib/files-cache/videos-caption-cache.ts b/server/lib/files-cache/video-captions-simple-file-cache.ts index d21acf4ef..cbeeff732 100644 --- a/server/lib/files-cache/videos-caption-cache.ts +++ b/server/lib/files-cache/video-captions-simple-file-cache.ts | |||
@@ -5,11 +5,11 @@ import { CONFIG } from '../../initializers/config' | |||
5 | import { FILES_CACHE } from '../../initializers/constants' | 5 | import { FILES_CACHE } from '../../initializers/constants' |
6 | import { VideoModel } from '../../models/video/video' | 6 | import { VideoModel } from '../../models/video/video' |
7 | import { VideoCaptionModel } from '../../models/video/video-caption' | 7 | import { VideoCaptionModel } from '../../models/video/video-caption' |
8 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' | 8 | import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache' |
9 | 9 | ||
10 | class VideosCaptionCache extends AbstractVideoStaticFileCache <string> { | 10 | class VideoCaptionsSimpleFileCache extends AbstractSimpleFileCache <string> { |
11 | 11 | ||
12 | private static instance: VideosCaptionCache | 12 | private static instance: VideoCaptionsSimpleFileCache |
13 | 13 | ||
14 | private constructor () { | 14 | private constructor () { |
15 | super() | 15 | super() |
@@ -23,7 +23,9 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <string> { | |||
23 | const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(filename) | 23 | const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(filename) |
24 | if (!videoCaption) return undefined | 24 | if (!videoCaption) return undefined |
25 | 25 | ||
26 | if (videoCaption.isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.filename) } | 26 | if (videoCaption.isOwned()) { |
27 | return { isOwned: true, path: join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.filename) } | ||
28 | } | ||
27 | 29 | ||
28 | return this.loadRemoteFile(filename) | 30 | return this.loadRemoteFile(filename) |
29 | } | 31 | } |
@@ -55,5 +57,5 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <string> { | |||
55 | } | 57 | } |
56 | 58 | ||
57 | export { | 59 | export { |
58 | VideosCaptionCache | 60 | VideoCaptionsSimpleFileCache |
59 | } | 61 | } |
diff --git a/server/lib/files-cache/video-miniature-permanent-file-cache.ts b/server/lib/files-cache/video-miniature-permanent-file-cache.ts new file mode 100644 index 000000000..35d9466f7 --- /dev/null +++ b/server/lib/files-cache/video-miniature-permanent-file-cache.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import { CONFIG } from '@server/initializers/config' | ||
2 | import { THUMBNAILS_SIZE } from '@server/initializers/constants' | ||
3 | import { ThumbnailModel } from '@server/models/video/thumbnail' | ||
4 | import { MThumbnail } from '@server/types/models' | ||
5 | import { ThumbnailType } from '@shared/models' | ||
6 | import { AbstractPermanentFileCache } from './shared' | ||
7 | |||
8 | export class VideoMiniaturePermanentFileCache extends AbstractPermanentFileCache<MThumbnail> { | ||
9 | |||
10 | constructor () { | ||
11 | super(CONFIG.STORAGE.THUMBNAILS_DIR) | ||
12 | } | ||
13 | |||
14 | protected loadModel (filename: string) { | ||
15 | return ThumbnailModel.loadByFilename(filename, ThumbnailType.MINIATURE) | ||
16 | } | ||
17 | |||
18 | protected getImageSize (image: MThumbnail): { width: number, height: number } { | ||
19 | if (image.width && image.height) { | ||
20 | return { | ||
21 | height: image.height, | ||
22 | width: image.width | ||
23 | } | ||
24 | } | ||
25 | |||
26 | return THUMBNAILS_SIZE | ||
27 | } | ||
28 | } | ||
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/video-previews-simple-file-cache.ts index d19c3f4f4..a05e80e16 100644 --- a/server/lib/files-cache/videos-preview-cache.ts +++ b/server/lib/files-cache/video-previews-simple-file-cache.ts | |||
@@ -1,15 +1,15 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { FILES_CACHE } from '../../initializers/constants' | 2 | import { FILES_CACHE } from '../../initializers/constants' |
3 | import { VideoModel } from '../../models/video/video' | 3 | import { VideoModel } from '../../models/video/video' |
4 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' | 4 | import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache' |
5 | import { doRequestAndSaveToFile } from '@server/helpers/requests' | 5 | import { doRequestAndSaveToFile } from '@server/helpers/requests' |
6 | import { ThumbnailModel } from '@server/models/video/thumbnail' | 6 | import { ThumbnailModel } from '@server/models/video/thumbnail' |
7 | import { ThumbnailType } from '@shared/models' | 7 | import { ThumbnailType } from '@shared/models' |
8 | import { logger } from '@server/helpers/logger' | 8 | import { logger } from '@server/helpers/logger' |
9 | 9 | ||
10 | class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { | 10 | class VideoPreviewsSimpleFileCache extends AbstractSimpleFileCache <string> { |
11 | 11 | ||
12 | private static instance: VideosPreviewCache | 12 | private static instance: VideoPreviewsSimpleFileCache |
13 | 13 | ||
14 | private constructor () { | 14 | private constructor () { |
15 | super() | 15 | super() |
@@ -54,5 +54,5 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { | |||
54 | } | 54 | } |
55 | 55 | ||
56 | export { | 56 | export { |
57 | VideosPreviewCache | 57 | VideoPreviewsSimpleFileCache |
58 | } | 58 | } |
diff --git a/server/lib/files-cache/video-storyboards-simple-file-cache.ts b/server/lib/files-cache/video-storyboards-simple-file-cache.ts new file mode 100644 index 000000000..4cd96e70c --- /dev/null +++ b/server/lib/files-cache/video-storyboards-simple-file-cache.ts | |||
@@ -0,0 +1,53 @@ | |||
1 | import { join } from 'path' | ||
2 | import { logger } from '@server/helpers/logger' | ||
3 | import { doRequestAndSaveToFile } from '@server/helpers/requests' | ||
4 | import { StoryboardModel } from '@server/models/video/storyboard' | ||
5 | import { FILES_CACHE } from '../../initializers/constants' | ||
6 | import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache' | ||
7 | |||
8 | class VideoStoryboardsSimpleFileCache extends AbstractSimpleFileCache <string> { | ||
9 | |||
10 | private static instance: VideoStoryboardsSimpleFileCache | ||
11 | |||
12 | private constructor () { | ||
13 | super() | ||
14 | } | ||
15 | |||
16 | static get Instance () { | ||
17 | return this.instance || (this.instance = new this()) | ||
18 | } | ||
19 | |||
20 | async getFilePathImpl (filename: string) { | ||
21 | const storyboard = await StoryboardModel.loadWithVideoByFilename(filename) | ||
22 | if (!storyboard) return undefined | ||
23 | |||
24 | if (storyboard.Video.isOwned()) return { isOwned: true, path: storyboard.getPath() } | ||
25 | |||
26 | return this.loadRemoteFile(storyboard.filename) | ||
27 | } | ||
28 | |||
29 | // Key is the storyboard filename | ||
30 | protected async loadRemoteFile (key: string) { | ||
31 | const storyboard = await StoryboardModel.loadWithVideoByFilename(key) | ||
32 | if (!storyboard) return undefined | ||
33 | |||
34 | const destPath = join(FILES_CACHE.STORYBOARDS.DIRECTORY, storyboard.filename) | ||
35 | const remoteUrl = storyboard.getOriginFileUrl(storyboard.Video) | ||
36 | |||
37 | try { | ||
38 | await doRequestAndSaveToFile(remoteUrl, destPath) | ||
39 | |||
40 | logger.debug('Fetched remote storyboard %s to %s.', remoteUrl, destPath) | ||
41 | |||
42 | return { isOwned: false, path: destPath } | ||
43 | } catch (err) { | ||
44 | logger.info('Cannot fetch remote storyboard file %s.', remoteUrl, { err }) | ||
45 | |||
46 | return undefined | ||
47 | } | ||
48 | } | ||
49 | } | ||
50 | |||
51 | export { | ||
52 | VideoStoryboardsSimpleFileCache | ||
53 | } | ||
diff --git a/server/lib/files-cache/videos-torrent-cache.ts b/server/lib/files-cache/video-torrents-simple-file-cache.ts index a6bf98dd4..8bcd0b9bf 100644 --- a/server/lib/files-cache/videos-torrent-cache.ts +++ b/server/lib/files-cache/video-torrents-simple-file-cache.ts | |||
@@ -6,11 +6,11 @@ import { MVideo, MVideoFile } from '@server/types/models' | |||
6 | import { CONFIG } from '../../initializers/config' | 6 | import { CONFIG } from '../../initializers/config' |
7 | import { FILES_CACHE } from '../../initializers/constants' | 7 | import { FILES_CACHE } from '../../initializers/constants' |
8 | import { VideoModel } from '../../models/video/video' | 8 | import { VideoModel } from '../../models/video/video' |
9 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' | 9 | import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache' |
10 | 10 | ||
11 | class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | 11 | class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache <string> { |
12 | 12 | ||
13 | private static instance: VideosTorrentCache | 13 | private static instance: VideoTorrentsSimpleFileCache |
14 | 14 | ||
15 | private constructor () { | 15 | private constructor () { |
16 | super() | 16 | super() |
@@ -66,5 +66,5 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | |||
66 | } | 66 | } |
67 | 67 | ||
68 | export { | 68 | export { |
69 | VideosTorrentCache | 69 | VideoTorrentsSimpleFileCache |
70 | } | 70 | } |
diff --git a/server/lib/hls.ts b/server/lib/hls.ts index fc1d7e1b0..19044d7c2 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts | |||
@@ -8,7 +8,7 @@ import { sha256 } from '@shared/extra-utils' | |||
8 | import { getVideoStreamDimensionsInfo } from '@shared/ffmpeg' | 8 | import { getVideoStreamDimensionsInfo } from '@shared/ffmpeg' |
9 | import { VideoStorage } from '@shared/models' | 9 | import { VideoStorage } from '@shared/models' |
10 | import { getAudioStreamCodec, getVideoStreamCodec } from '../helpers/ffmpeg' | 10 | import { getAudioStreamCodec, getVideoStreamCodec } from '../helpers/ffmpeg' |
11 | import { logger } from '../helpers/logger' | 11 | import { logger, loggerTagsFactory } from '../helpers/logger' |
12 | import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' | 12 | import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' |
13 | import { generateRandomString } from '../helpers/utils' | 13 | import { generateRandomString } from '../helpers/utils' |
14 | import { CONFIG } from '../initializers/config' | 14 | import { CONFIG } from '../initializers/config' |
@@ -20,6 +20,8 @@ import { storeHLSFileFromFilename } from './object-storage' | |||
20 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getHlsResolutionPlaylistFilename } from './paths' | 20 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getHlsResolutionPlaylistFilename } from './paths' |
21 | import { VideoPathManager } from './video-path-manager' | 21 | import { VideoPathManager } from './video-path-manager' |
22 | 22 | ||
23 | const lTags = loggerTagsFactory('hls') | ||
24 | |||
23 | async function updateStreamingPlaylistsInfohashesIfNeeded () { | 25 | async function updateStreamingPlaylistsInfohashesIfNeeded () { |
24 | const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion() | 26 | const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion() |
25 | 27 | ||
@@ -48,7 +50,7 @@ async function updatePlaylistAfterFileChange (video: MVideo, playlist: MStreamin | |||
48 | 50 | ||
49 | video.setHLSPlaylist(playlistWithFiles) | 51 | video.setHLSPlaylist(playlistWithFiles) |
50 | } catch (err) { | 52 | } catch (err) { |
51 | logger.info('Cannot update playlist after file change. Maybe due to concurrent transcoding', { err }) | 53 | logger.warn('Cannot update playlist after file change. Maybe due to concurrent transcoding', { err }) |
52 | } | 54 | } |
53 | } | 55 | } |
54 | 56 | ||
@@ -95,6 +97,8 @@ function updateMasterHLSPlaylist (video: MVideo, playlistArg: MStreamingPlaylist | |||
95 | const masterPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.playlistFilename) | 97 | const masterPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.playlistFilename) |
96 | await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') | 98 | await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') |
97 | 99 | ||
100 | logger.info('Updating %s master playlist file of video %s', masterPlaylistPath, video.uuid, lTags(video.uuid)) | ||
101 | |||
98 | if (playlist.storage === VideoStorage.OBJECT_STORAGE) { | 102 | if (playlist.storage === VideoStorage.OBJECT_STORAGE) { |
99 | playlist.playlistUrl = await storeHLSFileFromFilename(playlist, playlist.playlistFilename) | 103 | playlist.playlistUrl = await storeHLSFileFromFilename(playlist, playlist.playlistFilename) |
100 | await remove(masterPlaylistPath) | 104 | await remove(masterPlaylistPath) |
diff --git a/server/lib/job-queue/handlers/generate-storyboard.ts b/server/lib/job-queue/handlers/generate-storyboard.ts new file mode 100644 index 000000000..ec07c568c --- /dev/null +++ b/server/lib/job-queue/handlers/generate-storyboard.ts | |||
@@ -0,0 +1,149 @@ | |||
1 | import { Job } from 'bullmq' | ||
2 | import { join } from 'path' | ||
3 | import { getFFmpegCommandWrapperOptions } from '@server/helpers/ffmpeg' | ||
4 | import { generateImageFilename, getImageSize } from '@server/helpers/image-utils' | ||
5 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
6 | import { CONFIG } from '@server/initializers/config' | ||
7 | import { STORYBOARD } from '@server/initializers/constants' | ||
8 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | ||
9 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
10 | import { StoryboardModel } from '@server/models/video/storyboard' | ||
11 | import { VideoModel } from '@server/models/video/video' | ||
12 | import { MVideo } from '@server/types/models' | ||
13 | import { FFmpegImage, isAudioFile } from '@shared/ffmpeg' | ||
14 | import { GenerateStoryboardPayload } from '@shared/models' | ||
15 | |||
16 | const lTagsBase = loggerTagsFactory('storyboard') | ||
17 | |||
18 | async function processGenerateStoryboard (job: Job): Promise<void> { | ||
19 | const payload = job.data as GenerateStoryboardPayload | ||
20 | const lTags = lTagsBase(payload.videoUUID) | ||
21 | |||
22 | logger.info('Processing generate storyboard of %s in job %s.', payload.videoUUID, job.id, lTags) | ||
23 | |||
24 | const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(payload.videoUUID) | ||
25 | |||
26 | try { | ||
27 | const video = await VideoModel.loadFull(payload.videoUUID) | ||
28 | if (!video) { | ||
29 | logger.info('Video %s does not exist anymore, skipping storyboard generation.', payload.videoUUID, lTags) | ||
30 | return | ||
31 | } | ||
32 | |||
33 | const inputFile = video.getMaxQualityFile() | ||
34 | |||
35 | await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async videoPath => { | ||
36 | const isAudio = await isAudioFile(videoPath) | ||
37 | |||
38 | if (isAudio) { | ||
39 | logger.info('Do not generate a storyboard of %s since the video does not have a video stream', payload.videoUUID, lTags) | ||
40 | return | ||
41 | } | ||
42 | |||
43 | const ffmpeg = new FFmpegImage(getFFmpegCommandWrapperOptions('thumbnail')) | ||
44 | |||
45 | const filename = generateImageFilename() | ||
46 | const destination = join(CONFIG.STORAGE.STORYBOARDS_DIR, filename) | ||
47 | |||
48 | const totalSprites = buildTotalSprites(video) | ||
49 | if (totalSprites === 0) { | ||
50 | logger.info('Do not generate a storyboard of %s because the video is not long enough', payload.videoUUID, lTags) | ||
51 | return | ||
52 | } | ||
53 | |||
54 | const spriteDuration = Math.round(video.duration / totalSprites) | ||
55 | |||
56 | const spritesCount = findGridSize({ | ||
57 | toFind: totalSprites, | ||
58 | maxEdgeCount: STORYBOARD.SPRITES_MAX_EDGE_COUNT | ||
59 | }) | ||
60 | |||
61 | logger.debug( | ||
62 | 'Generating storyboard from video of %s to %s', video.uuid, destination, | ||
63 | { ...lTags, spritesCount, spriteDuration, videoDuration: video.duration } | ||
64 | ) | ||
65 | |||
66 | await ffmpeg.generateStoryboardFromVideo({ | ||
67 | destination, | ||
68 | path: videoPath, | ||
69 | sprites: { | ||
70 | size: STORYBOARD.SPRITE_SIZE, | ||
71 | count: spritesCount, | ||
72 | duration: spriteDuration | ||
73 | } | ||
74 | }) | ||
75 | |||
76 | const imageSize = await getImageSize(destination) | ||
77 | |||
78 | const existing = await StoryboardModel.loadByVideo(video.id) | ||
79 | if (existing) await existing.destroy() | ||
80 | |||
81 | await StoryboardModel.create({ | ||
82 | filename, | ||
83 | totalHeight: imageSize.height, | ||
84 | totalWidth: imageSize.width, | ||
85 | spriteHeight: STORYBOARD.SPRITE_SIZE.height, | ||
86 | spriteWidth: STORYBOARD.SPRITE_SIZE.width, | ||
87 | spriteDuration, | ||
88 | videoId: video.id | ||
89 | }) | ||
90 | |||
91 | logger.info('Storyboard generation %s ended for video %s.', destination, video.uuid, lTags) | ||
92 | }) | ||
93 | |||
94 | if (payload.federate) { | ||
95 | await federateVideoIfNeeded(video, false) | ||
96 | } | ||
97 | } finally { | ||
98 | inputFileMutexReleaser() | ||
99 | } | ||
100 | } | ||
101 | |||
102 | // --------------------------------------------------------------------------- | ||
103 | |||
104 | export { | ||
105 | processGenerateStoryboard | ||
106 | } | ||
107 | |||
108 | function buildTotalSprites (video: MVideo) { | ||
109 | const maxSprites = STORYBOARD.SPRITE_SIZE.height * STORYBOARD.SPRITE_SIZE.width | ||
110 | const totalSprites = Math.min(Math.ceil(video.duration), maxSprites) | ||
111 | |||
112 | // We can generate a single line | ||
113 | if (totalSprites <= STORYBOARD.SPRITES_MAX_EDGE_COUNT) return totalSprites | ||
114 | |||
115 | return findGridFit(totalSprites, STORYBOARD.SPRITES_MAX_EDGE_COUNT) | ||
116 | } | ||
117 | |||
118 | function findGridSize (options: { | ||
119 | toFind: number | ||
120 | maxEdgeCount: number | ||
121 | }) { | ||
122 | const { toFind, maxEdgeCount } = options | ||
123 | |||
124 | for (let i = 1; i <= maxEdgeCount; i++) { | ||
125 | for (let j = i; j <= maxEdgeCount; j++) { | ||
126 | if (toFind === i * j) return { width: j, height: i } | ||
127 | } | ||
128 | } | ||
129 | |||
130 | throw new Error(`Could not find grid size (to find: ${toFind}, max edge count: ${maxEdgeCount}`) | ||
131 | } | ||
132 | |||
133 | function findGridFit (value: number, maxMultiplier: number) { | ||
134 | for (let i = value; i--; i > 0) { | ||
135 | if (!isPrimeWithin(i, maxMultiplier)) return i | ||
136 | } | ||
137 | |||
138 | throw new Error('Could not find prime number below ' + value) | ||
139 | } | ||
140 | |||
141 | function isPrimeWithin (value: number, maxMultiplier: number) { | ||
142 | if (value < 2) return false | ||
143 | |||
144 | for (let i = 2, end = Math.min(Math.sqrt(value), maxMultiplier); i <= end; i++) { | ||
145 | if (value % i === 0 && value / i <= maxMultiplier) return false | ||
146 | } | ||
147 | |||
148 | return true | ||
149 | } | ||
diff --git a/server/lib/job-queue/handlers/move-to-object-storage.ts b/server/lib/job-queue/handlers/move-to-object-storage.ts index 26752ff37..9a99b6722 100644 --- a/server/lib/job-queue/handlers/move-to-object-storage.ts +++ b/server/lib/job-queue/handlers/move-to-object-storage.ts | |||
@@ -4,7 +4,7 @@ import { join } from 'path' | |||
4 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 4 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
5 | import { updateTorrentMetadata } from '@server/helpers/webtorrent' | 5 | import { updateTorrentMetadata } from '@server/helpers/webtorrent' |
6 | import { P2P_MEDIA_LOADER_PEER_VERSION } from '@server/initializers/constants' | 6 | import { P2P_MEDIA_LOADER_PEER_VERSION } from '@server/initializers/constants' |
7 | import { storeHLSFileFromFilename, storeWebTorrentFile } from '@server/lib/object-storage' | 7 | import { storeHLSFileFromFilename, storeWebVideoFile } from '@server/lib/object-storage' |
8 | import { getHLSDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' | 8 | import { getHLSDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' |
9 | import { VideoPathManager } from '@server/lib/video-path-manager' | 9 | import { VideoPathManager } from '@server/lib/video-path-manager' |
10 | import { moveToFailedMoveToObjectStorageState, moveToNextState } from '@server/lib/video-state' | 10 | import { moveToFailedMoveToObjectStorageState, moveToNextState } from '@server/lib/video-state' |
@@ -33,9 +33,9 @@ export async function processMoveToObjectStorage (job: Job) { | |||
33 | 33 | ||
34 | try { | 34 | try { |
35 | if (video.VideoFiles) { | 35 | if (video.VideoFiles) { |
36 | logger.debug('Moving %d webtorrent files for video %s.', video.VideoFiles.length, video.uuid, lTags) | 36 | logger.debug('Moving %d web video files for video %s.', video.VideoFiles.length, video.uuid, lTags) |
37 | 37 | ||
38 | await moveWebTorrentFiles(video) | 38 | await moveWebVideoFiles(video) |
39 | } | 39 | } |
40 | 40 | ||
41 | if (video.VideoStreamingPlaylists) { | 41 | if (video.VideoStreamingPlaylists) { |
@@ -75,11 +75,11 @@ export async function onMoveToObjectStorageFailure (job: Job, err: any) { | |||
75 | 75 | ||
76 | // --------------------------------------------------------------------------- | 76 | // --------------------------------------------------------------------------- |
77 | 77 | ||
78 | async function moveWebTorrentFiles (video: MVideoWithAllFiles) { | 78 | async function moveWebVideoFiles (video: MVideoWithAllFiles) { |
79 | for (const file of video.VideoFiles) { | 79 | for (const file of video.VideoFiles) { |
80 | if (file.storage !== VideoStorage.FILE_SYSTEM) continue | 80 | if (file.storage !== VideoStorage.FILE_SYSTEM) continue |
81 | 81 | ||
82 | const fileUrl = await storeWebTorrentFile(video, file) | 82 | const fileUrl = await storeWebVideoFile(video, file) |
83 | 83 | ||
84 | const oldPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, file) | 84 | const oldPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, file) |
85 | await onFileMoved({ videoOrPlaylist: video, file, fileUrl, oldPath }) | 85 | await onFileMoved({ videoOrPlaylist: video, file, fileUrl, oldPath }) |
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts index 9a4550e4d..d221e8968 100644 --- a/server/lib/job-queue/handlers/video-file-import.ts +++ b/server/lib/job-queue/handlers/video-file-import.ts | |||
@@ -3,7 +3,7 @@ import { copy, stat } from 'fs-extra' | |||
3 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 3 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
4 | import { CONFIG } from '@server/initializers/config' | 4 | import { CONFIG } from '@server/initializers/config' |
5 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | 5 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' |
6 | import { generateWebTorrentVideoFilename } from '@server/lib/paths' | 6 | import { generateWebVideoFilename } from '@server/lib/paths' |
7 | import { buildMoveToObjectStorageJob } from '@server/lib/video' | 7 | import { buildMoveToObjectStorageJob } from '@server/lib/video' |
8 | import { VideoPathManager } from '@server/lib/video-path-manager' | 8 | import { VideoPathManager } from '@server/lib/video-path-manager' |
9 | import { VideoModel } from '@server/models/video/video' | 9 | import { VideoModel } from '@server/models/video/video' |
@@ -56,7 +56,7 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) { | |||
56 | 56 | ||
57 | if (currentVideoFile) { | 57 | if (currentVideoFile) { |
58 | // Remove old file and old torrent | 58 | // Remove old file and old torrent |
59 | await video.removeWebTorrentFile(currentVideoFile) | 59 | await video.removeWebVideoFile(currentVideoFile) |
60 | // Remove the old video file from the array | 60 | // Remove the old video file from the array |
61 | video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile) | 61 | video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile) |
62 | 62 | ||
@@ -66,7 +66,7 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) { | |||
66 | const newVideoFile = new VideoFileModel({ | 66 | const newVideoFile = new VideoFileModel({ |
67 | resolution, | 67 | resolution, |
68 | extname: fileExt, | 68 | extname: fileExt, |
69 | filename: generateWebTorrentVideoFilename(resolution, fileExt), | 69 | filename: generateWebVideoFilename(resolution, fileExt), |
70 | storage: VideoStorage.FILE_SYSTEM, | 70 | storage: VideoStorage.FILE_SYSTEM, |
71 | size, | 71 | size, |
72 | fps, | 72 | fps, |
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index cdd362f6e..e5cd258d6 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -4,7 +4,7 @@ import { retryTransactionWrapper } from '@server/helpers/database-utils' | |||
4 | import { YoutubeDLWrapper } from '@server/helpers/youtube-dl' | 4 | import { YoutubeDLWrapper } from '@server/helpers/youtube-dl' |
5 | import { CONFIG } from '@server/initializers/config' | 5 | import { CONFIG } from '@server/initializers/config' |
6 | import { isPostImportVideoAccepted } from '@server/lib/moderation' | 6 | import { isPostImportVideoAccepted } from '@server/lib/moderation' |
7 | import { generateWebTorrentVideoFilename } from '@server/lib/paths' | 7 | import { generateWebVideoFilename } from '@server/lib/paths' |
8 | import { Hooks } from '@server/lib/plugins/hooks' | 8 | import { Hooks } from '@server/lib/plugins/hooks' |
9 | import { ServerConfigManager } from '@server/lib/server-config-manager' | 9 | import { ServerConfigManager } from '@server/lib/server-config-manager' |
10 | import { createOptimizeOrMergeAudioJobs } from '@server/lib/transcoding/create-transcoding-job' | 10 | import { createOptimizeOrMergeAudioJobs } from '@server/lib/transcoding/create-transcoding-job' |
@@ -39,7 +39,7 @@ import { VideoFileModel } from '../../../models/video/video-file' | |||
39 | import { VideoImportModel } from '../../../models/video/video-import' | 39 | import { VideoImportModel } from '../../../models/video/video-import' |
40 | import { federateVideoIfNeeded } from '../../activitypub/videos' | 40 | import { federateVideoIfNeeded } from '../../activitypub/videos' |
41 | import { Notifier } from '../../notifier' | 41 | import { Notifier } from '../../notifier' |
42 | import { generateVideoMiniature } from '../../thumbnail' | 42 | import { generateLocalVideoMiniature } from '../../thumbnail' |
43 | import { JobQueue } from '../job-queue' | 43 | import { JobQueue } from '../job-queue' |
44 | 44 | ||
45 | async function processVideoImport (job: Job): Promise<VideoImportPreventExceptionResult> { | 45 | async function processVideoImport (job: Job): Promise<VideoImportPreventExceptionResult> { |
@@ -148,7 +148,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid | |||
148 | extname: fileExt, | 148 | extname: fileExt, |
149 | resolution, | 149 | resolution, |
150 | size: stats.size, | 150 | size: stats.size, |
151 | filename: generateWebTorrentVideoFilename(resolution, fileExt), | 151 | filename: generateWebVideoFilename(resolution, fileExt), |
152 | fps, | 152 | fps, |
153 | videoId: videoImport.videoId | 153 | videoId: videoImport.videoId |
154 | } | 154 | } |
@@ -274,7 +274,7 @@ async function generateMiniature (videoImportWithFiles: MVideoImportDefaultFiles | |||
274 | } | 274 | } |
275 | } | 275 | } |
276 | 276 | ||
277 | const miniatureModel = await generateVideoMiniature({ | 277 | const miniatureModel = await generateLocalVideoMiniature({ |
278 | video: videoImportWithFiles.Video, | 278 | video: videoImportWithFiles.Video, |
279 | videoFile, | 279 | videoFile, |
280 | type: thumbnailType | 280 | type: thumbnailType |
@@ -306,6 +306,15 @@ async function afterImportSuccess (options: { | |||
306 | Notifier.Instance.notifyOnNewVideoIfNeeded(video) | 306 | Notifier.Instance.notifyOnNewVideoIfNeeded(video) |
307 | } | 307 | } |
308 | 308 | ||
309 | // Generate the storyboard in the job queue, and don't forget to federate an update after | ||
310 | await JobQueue.Instance.createJob({ | ||
311 | type: 'generate-video-storyboard' as 'generate-video-storyboard', | ||
312 | payload: { | ||
313 | videoUUID: video.uuid, | ||
314 | federate: true | ||
315 | } | ||
316 | }) | ||
317 | |||
309 | if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { | 318 | if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { |
310 | await JobQueue.Instance.createJob( | 319 | await JobQueue.Instance.createJob( |
311 | await buildMoveToObjectStorageJob({ video, previousVideoState: VideoState.TO_IMPORT }) | 320 | await buildMoveToObjectStorageJob({ video, previousVideoState: VideoState.TO_IMPORT }) |
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 49feb53f2..ae886de35 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -1,11 +1,13 @@ | |||
1 | import { Job } from 'bullmq' | 1 | import { Job } from 'bullmq' |
2 | import { readdir, remove } from 'fs-extra' | 2 | import { readdir, remove } from 'fs-extra' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { peertubeTruncate } from '@server/helpers/core-utils' | ||
5 | import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' | ||
4 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | 6 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' |
5 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | 7 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' |
6 | import { cleanupAndDestroyPermanentLive, cleanupTMPLiveFiles, cleanupUnsavedNormalLive } from '@server/lib/live' | 8 | import { cleanupAndDestroyPermanentLive, cleanupTMPLiveFiles, cleanupUnsavedNormalLive } from '@server/lib/live' |
7 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '@server/lib/paths' | 9 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '@server/lib/paths' |
8 | import { generateVideoMiniature } from '@server/lib/thumbnail' | 10 | import { generateLocalVideoMiniature } from '@server/lib/thumbnail' |
9 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/hls-transcoding' | 11 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/hls-transcoding' |
10 | import { VideoPathManager } from '@server/lib/video-path-manager' | 12 | import { VideoPathManager } from '@server/lib/video-path-manager' |
11 | import { moveToNextState } from '@server/lib/video-state' | 13 | import { moveToNextState } from '@server/lib/video-state' |
@@ -20,8 +22,7 @@ import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@serv | |||
20 | import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@shared/ffmpeg' | 22 | import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@shared/ffmpeg' |
21 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' | 23 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' |
22 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | 24 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
23 | import { peertubeTruncate } from '@server/helpers/core-utils' | 25 | import { JobQueue } from '../job-queue' |
24 | import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' | ||
25 | 26 | ||
26 | const lTags = loggerTagsFactory('live', 'job') | 27 | const lTags = loggerTagsFactory('live', 'job') |
27 | 28 | ||
@@ -142,11 +143,13 @@ async function saveReplayToExternalVideo (options: { | |||
142 | await remove(replayDirectory) | 143 | await remove(replayDirectory) |
143 | 144 | ||
144 | for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) { | 145 | for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) { |
145 | const image = await generateVideoMiniature({ video: replayVideo, videoFile: replayVideo.getMaxQualityFile(), type }) | 146 | const image = await generateLocalVideoMiniature({ video: replayVideo, videoFile: replayVideo.getMaxQualityFile(), type }) |
146 | await replayVideo.addAndSaveThumbnail(image) | 147 | await replayVideo.addAndSaveThumbnail(image) |
147 | } | 148 | } |
148 | 149 | ||
149 | await moveToNextState({ video: replayVideo, isNewVideo: true }) | 150 | await moveToNextState({ video: replayVideo, isNewVideo: true }) |
151 | |||
152 | await createStoryboardJob(replayVideo) | ||
150 | } | 153 | } |
151 | 154 | ||
152 | async function replaceLiveByReplay (options: { | 155 | async function replaceLiveByReplay (options: { |
@@ -186,6 +189,7 @@ async function replaceLiveByReplay (options: { | |||
186 | 189 | ||
187 | await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory }) | 190 | await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory }) |
188 | 191 | ||
192 | // FIXME: should not happen in this function | ||
189 | if (permanentLive) { // Remove session replay | 193 | if (permanentLive) { // Remove session replay |
190 | await remove(replayDirectory) | 194 | await remove(replayDirectory) |
191 | } else { // We won't stream again in this live, we can delete the base replay directory | 195 | } else { // We won't stream again in this live, we can delete the base replay directory |
@@ -194,7 +198,7 @@ async function replaceLiveByReplay (options: { | |||
194 | 198 | ||
195 | // Regenerate the thumbnail & preview? | 199 | // Regenerate the thumbnail & preview? |
196 | if (videoWithFiles.getMiniature().automaticallyGenerated === true) { | 200 | if (videoWithFiles.getMiniature().automaticallyGenerated === true) { |
197 | const miniature = await generateVideoMiniature({ | 201 | const miniature = await generateLocalVideoMiniature({ |
198 | video: videoWithFiles, | 202 | video: videoWithFiles, |
199 | videoFile: videoWithFiles.getMaxQualityFile(), | 203 | videoFile: videoWithFiles.getMaxQualityFile(), |
200 | type: ThumbnailType.MINIATURE | 204 | type: ThumbnailType.MINIATURE |
@@ -203,7 +207,7 @@ async function replaceLiveByReplay (options: { | |||
203 | } | 207 | } |
204 | 208 | ||
205 | if (videoWithFiles.getPreview().automaticallyGenerated === true) { | 209 | if (videoWithFiles.getPreview().automaticallyGenerated === true) { |
206 | const preview = await generateVideoMiniature({ | 210 | const preview = await generateLocalVideoMiniature({ |
207 | video: videoWithFiles, | 211 | video: videoWithFiles, |
208 | videoFile: videoWithFiles.getMaxQualityFile(), | 212 | videoFile: videoWithFiles.getMaxQualityFile(), |
209 | type: ThumbnailType.PREVIEW | 213 | type: ThumbnailType.PREVIEW |
@@ -213,6 +217,8 @@ async function replaceLiveByReplay (options: { | |||
213 | 217 | ||
214 | // We consider this is a new video | 218 | // We consider this is a new video |
215 | await moveToNextState({ video: videoWithFiles, isNewVideo: true }) | 219 | await moveToNextState({ video: videoWithFiles, isNewVideo: true }) |
220 | |||
221 | await createStoryboardJob(videoWithFiles) | ||
216 | } | 222 | } |
217 | 223 | ||
218 | async function assignReplayFilesToVideo (options: { | 224 | async function assignReplayFilesToVideo (options: { |
@@ -277,3 +283,13 @@ async function cleanupLiveAndFederate (options: { | |||
277 | logger.warn('Cannot federate live after cleanup', { videoId: video.id, err }) | 283 | logger.warn('Cannot federate live after cleanup', { videoId: video.id, err }) |
278 | } | 284 | } |
279 | } | 285 | } |
286 | |||
287 | function createStoryboardJob (video: MVideo) { | ||
288 | return JobQueue.Instance.createJob({ | ||
289 | type: 'generate-video-storyboard' as 'generate-video-storyboard', | ||
290 | payload: { | ||
291 | videoUUID: video.uuid, | ||
292 | federate: true | ||
293 | } | ||
294 | }) | ||
295 | } | ||
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index f8758f170..1c8f4fd9f 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Job } from 'bullmq' | 1 | import { Job } from 'bullmq' |
2 | import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding' | 2 | import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding' |
3 | import { generateHlsPlaylistResolution } from '@server/lib/transcoding/hls-transcoding' | 3 | import { generateHlsPlaylistResolution } from '@server/lib/transcoding/hls-transcoding' |
4 | import { mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewWebTorrentResolution } from '@server/lib/transcoding/web-transcoding' | 4 | import { mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewWebVideoResolution } from '@server/lib/transcoding/web-transcoding' |
5 | import { removeAllWebTorrentFiles } from '@server/lib/video-file' | 5 | import { removeAllWebVideoFiles } from '@server/lib/video-file' |
6 | import { VideoPathManager } from '@server/lib/video-path-manager' | 6 | import { VideoPathManager } from '@server/lib/video-path-manager' |
7 | import { moveToFailedTranscodingState } from '@server/lib/video-state' | 7 | import { moveToFailedTranscodingState } from '@server/lib/video-state' |
8 | import { UserModel } from '@server/models/user/user' | 8 | import { UserModel } from '@server/models/user/user' |
@@ -11,7 +11,7 @@ import { MUser, MUserId, MVideoFullLight } from '@server/types/models' | |||
11 | import { | 11 | import { |
12 | HLSTranscodingPayload, | 12 | HLSTranscodingPayload, |
13 | MergeAudioTranscodingPayload, | 13 | MergeAudioTranscodingPayload, |
14 | NewWebTorrentResolutionTranscodingPayload, | 14 | NewWebVideoResolutionTranscodingPayload, |
15 | OptimizeTranscodingPayload, | 15 | OptimizeTranscodingPayload, |
16 | VideoTranscodingPayload | 16 | VideoTranscodingPayload |
17 | } from '@shared/models' | 17 | } from '@shared/models' |
@@ -22,9 +22,9 @@ type HandlerFunction = (job: Job, payload: VideoTranscodingPayload, video: MVide | |||
22 | 22 | ||
23 | const handlers: { [ id in VideoTranscodingPayload['type'] ]: HandlerFunction } = { | 23 | const handlers: { [ id in VideoTranscodingPayload['type'] ]: HandlerFunction } = { |
24 | 'new-resolution-to-hls': handleHLSJob, | 24 | 'new-resolution-to-hls': handleHLSJob, |
25 | 'new-resolution-to-webtorrent': handleNewWebTorrentResolutionJob, | 25 | 'new-resolution-to-web-video': handleNewWebVideoResolutionJob, |
26 | 'merge-audio-to-webtorrent': handleWebTorrentMergeAudioJob, | 26 | 'merge-audio-to-web-video': handleWebVideoMergeAudioJob, |
27 | 'optimize-to-webtorrent': handleWebTorrentOptimizeJob | 27 | 'optimize-to-web-video': handleWebVideoOptimizeJob |
28 | } | 28 | } |
29 | 29 | ||
30 | const lTags = loggerTagsFactory('transcoding') | 30 | const lTags = loggerTagsFactory('transcoding') |
@@ -74,7 +74,7 @@ export { | |||
74 | // Job handlers | 74 | // Job handlers |
75 | // --------------------------------------------------------------------------- | 75 | // --------------------------------------------------------------------------- |
76 | 76 | ||
77 | async function handleWebTorrentMergeAudioJob (job: Job, payload: MergeAudioTranscodingPayload, video: MVideoFullLight, user: MUserId) { | 77 | async function handleWebVideoMergeAudioJob (job: Job, payload: MergeAudioTranscodingPayload, video: MVideoFullLight, user: MUserId) { |
78 | logger.info('Handling merge audio transcoding job for %s.', video.uuid, lTags(video.uuid), { payload }) | 78 | logger.info('Handling merge audio transcoding job for %s.', video.uuid, lTags(video.uuid), { payload }) |
79 | 79 | ||
80 | await mergeAudioVideofile({ video, resolution: payload.resolution, fps: payload.fps, job }) | 80 | await mergeAudioVideofile({ video, resolution: payload.resolution, fps: payload.fps, job }) |
@@ -84,7 +84,7 @@ async function handleWebTorrentMergeAudioJob (job: Job, payload: MergeAudioTrans | |||
84 | await onTranscodingEnded({ isNewVideo: payload.isNewVideo, moveVideoToNextState: !payload.hasChildren, video }) | 84 | await onTranscodingEnded({ isNewVideo: payload.isNewVideo, moveVideoToNextState: !payload.hasChildren, video }) |
85 | } | 85 | } |
86 | 86 | ||
87 | async function handleWebTorrentOptimizeJob (job: Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight, user: MUserId) { | 87 | async function handleWebVideoOptimizeJob (job: Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight, user: MUserId) { |
88 | logger.info('Handling optimize transcoding job for %s.', video.uuid, lTags(video.uuid), { payload }) | 88 | logger.info('Handling optimize transcoding job for %s.', video.uuid, lTags(video.uuid), { payload }) |
89 | 89 | ||
90 | await optimizeOriginalVideofile({ video, inputVideoFile: video.getMaxQualityFile(), quickTranscode: payload.quickTranscode, job }) | 90 | await optimizeOriginalVideofile({ video, inputVideoFile: video.getMaxQualityFile(), quickTranscode: payload.quickTranscode, job }) |
@@ -96,12 +96,12 @@ async function handleWebTorrentOptimizeJob (job: Job, payload: OptimizeTranscodi | |||
96 | 96 | ||
97 | // --------------------------------------------------------------------------- | 97 | // --------------------------------------------------------------------------- |
98 | 98 | ||
99 | async function handleNewWebTorrentResolutionJob (job: Job, payload: NewWebTorrentResolutionTranscodingPayload, video: MVideoFullLight) { | 99 | async function handleNewWebVideoResolutionJob (job: Job, payload: NewWebVideoResolutionTranscodingPayload, video: MVideoFullLight) { |
100 | logger.info('Handling WebTorrent transcoding job for %s.', video.uuid, lTags(video.uuid), { payload }) | 100 | logger.info('Handling Web Video transcoding job for %s.', video.uuid, lTags(video.uuid), { payload }) |
101 | 101 | ||
102 | await transcodeNewWebTorrentResolution({ video, resolution: payload.resolution, fps: payload.fps, job }) | 102 | await transcodeNewWebVideoResolution({ video, resolution: payload.resolution, fps: payload.fps, job }) |
103 | 103 | ||
104 | logger.info('WebTorrent transcoding job for %s ended.', video.uuid, lTags(video.uuid), { payload }) | 104 | logger.info('Web Video transcoding job for %s ended.', video.uuid, lTags(video.uuid), { payload }) |
105 | 105 | ||
106 | await onTranscodingEnded({ isNewVideo: payload.isNewVideo, moveVideoToNextState: true, video }) | 106 | await onTranscodingEnded({ isNewVideo: payload.isNewVideo, moveVideoToNextState: true, video }) |
107 | } | 107 | } |
@@ -118,7 +118,7 @@ async function handleHLSJob (job: Job, payload: HLSTranscodingPayload, videoArg: | |||
118 | video = await VideoModel.loadFull(videoArg.uuid) | 118 | video = await VideoModel.loadFull(videoArg.uuid) |
119 | 119 | ||
120 | const videoFileInput = payload.copyCodecs | 120 | const videoFileInput = payload.copyCodecs |
121 | ? video.getWebTorrentFile(payload.resolution) | 121 | ? video.getWebVideoFile(payload.resolution) |
122 | : video.getMaxQualityFile() | 122 | : video.getMaxQualityFile() |
123 | 123 | ||
124 | const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist() | 124 | const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist() |
@@ -140,10 +140,10 @@ async function handleHLSJob (job: Job, payload: HLSTranscodingPayload, videoArg: | |||
140 | 140 | ||
141 | logger.info('HLS transcoding job for %s ended.', video.uuid, lTags(video.uuid), { payload }) | 141 | logger.info('HLS transcoding job for %s ended.', video.uuid, lTags(video.uuid), { payload }) |
142 | 142 | ||
143 | if (payload.deleteWebTorrentFiles === true) { | 143 | if (payload.deleteWebVideoFiles === true) { |
144 | logger.info('Removing WebTorrent files of %s now we have a HLS version of it.', video.uuid, lTags(video.uuid)) | 144 | logger.info('Removing Web Video files of %s now we have a HLS version of it.', video.uuid, lTags(video.uuid)) |
145 | 145 | ||
146 | await removeAllWebTorrentFiles(video) | 146 | await removeAllWebVideoFiles(video) |
147 | } | 147 | } |
148 | 148 | ||
149 | await onTranscodingEnded({ isNewVideo: payload.isNewVideo, moveVideoToNextState: true, video }) | 149 | await onTranscodingEnded({ isNewVideo: payload.isNewVideo, moveVideoToNextState: true, video }) |
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index 03f6fbea7..177bca285 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -25,6 +25,7 @@ import { | |||
25 | DeleteResumableUploadMetaFilePayload, | 25 | DeleteResumableUploadMetaFilePayload, |
26 | EmailPayload, | 26 | EmailPayload, |
27 | FederateVideoPayload, | 27 | FederateVideoPayload, |
28 | GenerateStoryboardPayload, | ||
28 | JobState, | 29 | JobState, |
29 | JobType, | 30 | JobType, |
30 | ManageVideoTorrentPayload, | 31 | ManageVideoTorrentPayload, |
@@ -65,6 +66,7 @@ import { processVideoLiveEnding } from './handlers/video-live-ending' | |||
65 | import { processVideoStudioEdition } from './handlers/video-studio-edition' | 66 | import { processVideoStudioEdition } from './handlers/video-studio-edition' |
66 | import { processVideoTranscoding } from './handlers/video-transcoding' | 67 | import { processVideoTranscoding } from './handlers/video-transcoding' |
67 | import { processVideosViewsStats } from './handlers/video-views-stats' | 68 | import { processVideosViewsStats } from './handlers/video-views-stats' |
69 | import { processGenerateStoryboard } from './handlers/generate-storyboard' | ||
68 | 70 | ||
69 | export type CreateJobArgument = | 71 | export type CreateJobArgument = |
70 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | | 72 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | |
@@ -91,7 +93,8 @@ export type CreateJobArgument = | |||
91 | { type: 'after-video-channel-import', payload: AfterVideoChannelImportPayload } | | 93 | { type: 'after-video-channel-import', payload: AfterVideoChannelImportPayload } | |
92 | { type: 'notify', payload: NotifyPayload } | | 94 | { type: 'notify', payload: NotifyPayload } | |
93 | { type: 'move-to-object-storage', payload: MoveObjectStoragePayload } | | 95 | { type: 'move-to-object-storage', payload: MoveObjectStoragePayload } | |
94 | { type: 'federate-video', payload: FederateVideoPayload } | 96 | { type: 'federate-video', payload: FederateVideoPayload } | |
97 | { type: 'generate-video-storyboard', payload: GenerateStoryboardPayload } | ||
95 | 98 | ||
96 | export type CreateJobOptions = { | 99 | export type CreateJobOptions = { |
97 | delay?: number | 100 | delay?: number |
@@ -122,7 +125,8 @@ const handlers: { [id in JobType]: (job: Job) => Promise<any> } = { | |||
122 | 'video-redundancy': processVideoRedundancy, | 125 | 'video-redundancy': processVideoRedundancy, |
123 | 'video-studio-edition': processVideoStudioEdition, | 126 | 'video-studio-edition': processVideoStudioEdition, |
124 | 'video-transcoding': processVideoTranscoding, | 127 | 'video-transcoding': processVideoTranscoding, |
125 | 'videos-views-stats': processVideosViewsStats | 128 | 'videos-views-stats': processVideosViewsStats, |
129 | 'generate-video-storyboard': processGenerateStoryboard | ||
126 | } | 130 | } |
127 | 131 | ||
128 | const errorHandlers: { [id in JobType]?: (job: Job, err: any) => Promise<any> } = { | 132 | const errorHandlers: { [id in JobType]?: (job: Job, err: any) => Promise<any> } = { |
@@ -141,10 +145,11 @@ const jobTypes: JobType[] = [ | |||
141 | 'after-video-channel-import', | 145 | 'after-video-channel-import', |
142 | 'email', | 146 | 'email', |
143 | 'federate-video', | 147 | 'federate-video', |
144 | 'transcoding-job-builder', | 148 | 'generate-video-storyboard', |
145 | 'manage-video-torrent', | 149 | 'manage-video-torrent', |
146 | 'move-to-object-storage', | 150 | 'move-to-object-storage', |
147 | 'notify', | 151 | 'notify', |
152 | 'transcoding-job-builder', | ||
148 | 'video-channel-import', | 153 | 'video-channel-import', |
149 | 'video-file-import', | 154 | 'video-file-import', |
150 | 'video-import', | 155 | 'video-import', |
diff --git a/server/lib/local-actor.ts b/server/lib/local-actor.ts index 16dc265a3..611e6d0af 100644 --- a/server/lib/local-actor.ts +++ b/server/lib/local-actor.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { remove } from 'fs-extra' | 1 | import { remove } from 'fs-extra' |
2 | import { LRUCache } from 'lru-cache' | ||
3 | import { join } from 'path' | 2 | import { join } from 'path' |
4 | import { Transaction } from 'sequelize/types' | 3 | import { Transaction } from 'sequelize/types' |
5 | import { ActorModel } from '@server/models/actor/actor' | 4 | import { ActorModel } from '@server/models/actor/actor' |
@@ -8,14 +7,14 @@ import { buildUUID } from '@shared/extra-utils' | |||
8 | import { ActivityPubActorType, ActorImageType } from '@shared/models' | 7 | import { ActivityPubActorType, ActorImageType } from '@shared/models' |
9 | import { retryTransactionWrapper } from '../helpers/database-utils' | 8 | import { retryTransactionWrapper } from '../helpers/database-utils' |
10 | import { CONFIG } from '../initializers/config' | 9 | import { CONFIG } from '../initializers/config' |
11 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, WEBSERVER } from '../initializers/constants' | 10 | import { ACTOR_IMAGES_SIZE, WEBSERVER } from '../initializers/constants' |
12 | import { sequelizeTypescript } from '../initializers/database' | 11 | import { sequelizeTypescript } from '../initializers/database' |
13 | import { MAccountDefault, MActor, MChannelDefault } from '../types/models' | 12 | import { MAccountDefault, MActor, MChannelDefault } from '../types/models' |
14 | import { deleteActorImages, updateActorImages } from './activitypub/actors' | 13 | import { deleteActorImages, updateActorImages } from './activitypub/actors' |
15 | import { sendUpdateActor } from './activitypub/send' | 14 | import { sendUpdateActor } from './activitypub/send' |
16 | import { downloadImageFromWorker, processImageFromWorker } from './worker/parent-process' | 15 | import { processImageFromWorker } from './worker/parent-process' |
17 | 16 | ||
18 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { | 17 | export function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { |
19 | return new ActorModel({ | 18 | return new ActorModel({ |
20 | type, | 19 | type, |
21 | url, | 20 | url, |
@@ -32,7 +31,7 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU | |||
32 | }) as MActor | 31 | }) as MActor |
33 | } | 32 | } |
34 | 33 | ||
35 | async function updateLocalActorImageFiles ( | 34 | export async function updateLocalActorImageFiles ( |
36 | accountOrChannel: MAccountDefault | MChannelDefault, | 35 | accountOrChannel: MAccountDefault | MChannelDefault, |
37 | imagePhysicalFile: Express.Multer.File, | 36 | imagePhysicalFile: Express.Multer.File, |
38 | type: ActorImageType | 37 | type: ActorImageType |
@@ -41,7 +40,7 @@ async function updateLocalActorImageFiles ( | |||
41 | const extension = getLowercaseExtension(imagePhysicalFile.filename) | 40 | const extension = getLowercaseExtension(imagePhysicalFile.filename) |
42 | 41 | ||
43 | const imageName = buildUUID() + extension | 42 | const imageName = buildUUID() + extension |
44 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) | 43 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES_DIR, imageName) |
45 | await processImageFromWorker({ path: imagePhysicalFile.path, destination, newSize: imageSize, keepOriginal: true }) | 44 | await processImageFromWorker({ path: imagePhysicalFile.path, destination, newSize: imageSize, keepOriginal: true }) |
46 | 45 | ||
47 | return { | 46 | return { |
@@ -73,7 +72,7 @@ async function updateLocalActorImageFiles ( | |||
73 | })) | 72 | })) |
74 | } | 73 | } |
75 | 74 | ||
76 | async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { | 75 | export async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { |
77 | return retryTransactionWrapper(() => { | 76 | return retryTransactionWrapper(() => { |
78 | return sequelizeTypescript.transaction(async t => { | 77 | return sequelizeTypescript.transaction(async t => { |
79 | const updatedActor = await deleteActorImages(accountOrChannel.Actor, type, t) | 78 | const updatedActor = await deleteActorImages(accountOrChannel.Actor, type, t) |
@@ -88,7 +87,7 @@ async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MC | |||
88 | 87 | ||
89 | // --------------------------------------------------------------------------- | 88 | // --------------------------------------------------------------------------- |
90 | 89 | ||
91 | async function findAvailableLocalActorName (baseActorName: string, transaction?: Transaction) { | 90 | export async function findAvailableLocalActorName (baseActorName: string, transaction?: Transaction) { |
92 | let actor = await ActorModel.loadLocalByName(baseActorName, transaction) | 91 | let actor = await ActorModel.loadLocalByName(baseActorName, transaction) |
93 | if (!actor) return baseActorName | 92 | if (!actor) return baseActorName |
94 | 93 | ||
@@ -101,34 +100,3 @@ async function findAvailableLocalActorName (baseActorName: string, transaction?: | |||
101 | 100 | ||
102 | throw new Error('Cannot find available actor local name (too much iterations).') | 101 | throw new Error('Cannot find available actor local name (too much iterations).') |
103 | } | 102 | } |
104 | |||
105 | // --------------------------------------------------------------------------- | ||
106 | |||
107 | function downloadActorImageFromWorker (options: { | ||
108 | fileUrl: string | ||
109 | filename: string | ||
110 | type: ActorImageType | ||
111 | size: typeof ACTOR_IMAGES_SIZE[ActorImageType][0] | ||
112 | }) { | ||
113 | const downloaderOptions = { | ||
114 | url: options.fileUrl, | ||
115 | destDir: CONFIG.STORAGE.ACTOR_IMAGES, | ||
116 | destName: options.filename, | ||
117 | size: options.size | ||
118 | } | ||
119 | |||
120 | return downloadImageFromWorker(downloaderOptions) | ||
121 | } | ||
122 | |||
123 | // Unsafe so could returns paths that does not exist anymore | ||
124 | const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.ACTOR_IMAGE_STATIC.MAX_SIZE }) | ||
125 | |||
126 | export { | ||
127 | actorImagePathUnsafeCache, | ||
128 | updateLocalActorImageFiles, | ||
129 | findAvailableLocalActorName, | ||
130 | downloadActorImageFromWorker, | ||
131 | deleteLocalActorImageFile, | ||
132 | downloadImageFromWorker, | ||
133 | buildActorInstance | ||
134 | } | ||
diff --git a/server/lib/object-storage/index.ts b/server/lib/object-storage/index.ts index 6525f8dfb..3ad6cab63 100644 --- a/server/lib/object-storage/index.ts +++ b/server/lib/object-storage/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './keys' | 1 | export * from './keys' |
2 | export * from './proxy' | 2 | export * from './proxy' |
3 | export * from './pre-signed-urls' | ||
3 | export * from './urls' | 4 | export * from './urls' |
4 | export * from './videos' | 5 | export * from './videos' |
diff --git a/server/lib/object-storage/keys.ts b/server/lib/object-storage/keys.ts index 4f17073f4..6d2098298 100644 --- a/server/lib/object-storage/keys.ts +++ b/server/lib/object-storage/keys.ts | |||
@@ -9,12 +9,12 @@ function generateHLSObjectBaseStorageKey (playlist: MStreamingPlaylistVideo) { | |||
9 | return join(playlist.getStringType(), playlist.Video.uuid) | 9 | return join(playlist.getStringType(), playlist.Video.uuid) |
10 | } | 10 | } |
11 | 11 | ||
12 | function generateWebTorrentObjectStorageKey (filename: string) { | 12 | function generateWebVideoObjectStorageKey (filename: string) { |
13 | return filename | 13 | return filename |
14 | } | 14 | } |
15 | 15 | ||
16 | export { | 16 | export { |
17 | generateHLSObjectStorageKey, | 17 | generateHLSObjectStorageKey, |
18 | generateHLSObjectBaseStorageKey, | 18 | generateHLSObjectBaseStorageKey, |
19 | generateWebTorrentObjectStorageKey | 19 | generateWebVideoObjectStorageKey |
20 | } | 20 | } |
diff --git a/server/lib/object-storage/pre-signed-urls.ts b/server/lib/object-storage/pre-signed-urls.ts new file mode 100644 index 000000000..caf149bb8 --- /dev/null +++ b/server/lib/object-storage/pre-signed-urls.ts | |||
@@ -0,0 +1,46 @@ | |||
1 | import { GetObjectCommand } from '@aws-sdk/client-s3' | ||
2 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner' | ||
3 | import { CONFIG } from '@server/initializers/config' | ||
4 | import { MStreamingPlaylistVideo, MVideoFile } from '@server/types/models' | ||
5 | import { generateHLSObjectStorageKey, generateWebVideoObjectStorageKey } from './keys' | ||
6 | import { buildKey, getClient } from './shared' | ||
7 | import { getHLSPublicFileUrl, getWebVideoPublicFileUrl } from './urls' | ||
8 | |||
9 | export async function generateWebVideoPresignedUrl (options: { | ||
10 | file: MVideoFile | ||
11 | downloadFilename: string | ||
12 | }) { | ||
13 | const { file, downloadFilename } = options | ||
14 | |||
15 | const key = generateWebVideoObjectStorageKey(file.filename) | ||
16 | |||
17 | const command = new GetObjectCommand({ | ||
18 | Bucket: CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BUCKET_NAME, | ||
19 | Key: buildKey(key, CONFIG.OBJECT_STORAGE.WEB_VIDEOS), | ||
20 | ResponseContentDisposition: `attachment; filename=${downloadFilename}` | ||
21 | }) | ||
22 | |||
23 | const url = await getSignedUrl(getClient(), command, { expiresIn: 3600 * 24 }) | ||
24 | |||
25 | return getWebVideoPublicFileUrl(url) | ||
26 | } | ||
27 | |||
28 | export async function generateHLSFilePresignedUrl (options: { | ||
29 | streamingPlaylist: MStreamingPlaylistVideo | ||
30 | file: MVideoFile | ||
31 | downloadFilename: string | ||
32 | }) { | ||
33 | const { streamingPlaylist, file, downloadFilename } = options | ||
34 | |||
35 | const key = generateHLSObjectStorageKey(streamingPlaylist, file.filename) | ||
36 | |||
37 | const command = new GetObjectCommand({ | ||
38 | Bucket: CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET_NAME, | ||
39 | Key: buildKey(key, CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS), | ||
40 | ResponseContentDisposition: `attachment; filename=${downloadFilename}` | ||
41 | }) | ||
42 | |||
43 | const url = await getSignedUrl(getClient(), command, { expiresIn: 3600 * 24 }) | ||
44 | |||
45 | return getHLSPublicFileUrl(url) | ||
46 | } | ||
diff --git a/server/lib/object-storage/proxy.ts b/server/lib/object-storage/proxy.ts index c782a8a25..c09a0d1b0 100644 --- a/server/lib/object-storage/proxy.ts +++ b/server/lib/object-storage/proxy.ts | |||
@@ -7,19 +7,19 @@ import { StreamReplacer } from '@server/helpers/stream-replacer' | |||
7 | import { MStreamingPlaylist, MVideo } from '@server/types/models' | 7 | import { MStreamingPlaylist, MVideo } from '@server/types/models' |
8 | import { HttpStatusCode } from '@shared/models' | 8 | import { HttpStatusCode } from '@shared/models' |
9 | import { injectQueryToPlaylistUrls } from '../hls' | 9 | import { injectQueryToPlaylistUrls } from '../hls' |
10 | import { getHLSFileReadStream, getWebTorrentFileReadStream } from './videos' | 10 | import { getHLSFileReadStream, getWebVideoFileReadStream } from './videos' |
11 | 11 | ||
12 | export async function proxifyWebTorrentFile (options: { | 12 | export async function proxifyWebVideoFile (options: { |
13 | req: express.Request | 13 | req: express.Request |
14 | res: express.Response | 14 | res: express.Response |
15 | filename: string | 15 | filename: string |
16 | }) { | 16 | }) { |
17 | const { req, res, filename } = options | 17 | const { req, res, filename } = options |
18 | 18 | ||
19 | logger.debug('Proxifying WebTorrent file %s from object storage.', filename) | 19 | logger.debug('Proxifying Web Video file %s from object storage.', filename) |
20 | 20 | ||
21 | try { | 21 | try { |
22 | const { response: s3Response, stream } = await getWebTorrentFileReadStream({ | 22 | const { response: s3Response, stream } = await getWebVideoFileReadStream({ |
23 | filename, | 23 | filename, |
24 | rangeHeader: req.header('range') | 24 | rangeHeader: req.header('range') |
25 | }) | 25 | }) |
diff --git a/server/lib/object-storage/urls.ts b/server/lib/object-storage/urls.ts index b8ef94559..40619cd5a 100644 --- a/server/lib/object-storage/urls.ts +++ b/server/lib/object-storage/urls.ts | |||
@@ -9,8 +9,8 @@ function getInternalUrl (config: BucketInfo, keyWithoutPrefix: string) { | |||
9 | 9 | ||
10 | // --------------------------------------------------------------------------- | 10 | // --------------------------------------------------------------------------- |
11 | 11 | ||
12 | function getWebTorrentPublicFileUrl (fileUrl: string) { | 12 | function getWebVideoPublicFileUrl (fileUrl: string) { |
13 | const baseUrl = CONFIG.OBJECT_STORAGE.VIDEOS.BASE_URL | 13 | const baseUrl = CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BASE_URL |
14 | if (!baseUrl) return fileUrl | 14 | if (!baseUrl) return fileUrl |
15 | 15 | ||
16 | return replaceByBaseUrl(fileUrl, baseUrl) | 16 | return replaceByBaseUrl(fileUrl, baseUrl) |
@@ -29,8 +29,8 @@ function getHLSPrivateFileUrl (video: MVideoUUID, filename: string) { | |||
29 | return WEBSERVER.URL + OBJECT_STORAGE_PROXY_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + video.uuid + `/${filename}` | 29 | return WEBSERVER.URL + OBJECT_STORAGE_PROXY_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + video.uuid + `/${filename}` |
30 | } | 30 | } |
31 | 31 | ||
32 | function getWebTorrentPrivateFileUrl (filename: string) { | 32 | function getWebVideoPrivateFileUrl (filename: string) { |
33 | return WEBSERVER.URL + OBJECT_STORAGE_PROXY_PATHS.PRIVATE_WEBSEED + filename | 33 | return WEBSERVER.URL + OBJECT_STORAGE_PROXY_PATHS.PRIVATE_WEB_VIDEOS + filename |
34 | } | 34 | } |
35 | 35 | ||
36 | // --------------------------------------------------------------------------- | 36 | // --------------------------------------------------------------------------- |
@@ -38,11 +38,11 @@ function getWebTorrentPrivateFileUrl (filename: string) { | |||
38 | export { | 38 | export { |
39 | getInternalUrl, | 39 | getInternalUrl, |
40 | 40 | ||
41 | getWebTorrentPublicFileUrl, | 41 | getWebVideoPublicFileUrl, |
42 | getHLSPublicFileUrl, | 42 | getHLSPublicFileUrl, |
43 | 43 | ||
44 | getHLSPrivateFileUrl, | 44 | getHLSPrivateFileUrl, |
45 | getWebTorrentPrivateFileUrl, | 45 | getWebVideoPrivateFileUrl, |
46 | 46 | ||
47 | replaceByBaseUrl | 47 | replaceByBaseUrl |
48 | } | 48 | } |
diff --git a/server/lib/object-storage/videos.ts b/server/lib/object-storage/videos.ts index 9152c5352..891e9ff76 100644 --- a/server/lib/object-storage/videos.ts +++ b/server/lib/object-storage/videos.ts | |||
@@ -4,7 +4,7 @@ import { CONFIG } from '@server/initializers/config' | |||
4 | import { MStreamingPlaylistVideo, MVideo, MVideoFile } from '@server/types/models' | 4 | import { MStreamingPlaylistVideo, MVideo, MVideoFile } from '@server/types/models' |
5 | import { getHLSDirectory } from '../paths' | 5 | import { getHLSDirectory } from '../paths' |
6 | import { VideoPathManager } from '../video-path-manager' | 6 | import { VideoPathManager } from '../video-path-manager' |
7 | import { generateHLSObjectBaseStorageKey, generateHLSObjectStorageKey, generateWebTorrentObjectStorageKey } from './keys' | 7 | import { generateHLSObjectBaseStorageKey, generateHLSObjectStorageKey, generateWebVideoObjectStorageKey } from './keys' |
8 | import { | 8 | import { |
9 | createObjectReadStream, | 9 | createObjectReadStream, |
10 | listKeysOfPrefix, | 10 | listKeysOfPrefix, |
@@ -55,21 +55,21 @@ function storeHLSFileFromContent (playlist: MStreamingPlaylistVideo, path: strin | |||
55 | 55 | ||
56 | // --------------------------------------------------------------------------- | 56 | // --------------------------------------------------------------------------- |
57 | 57 | ||
58 | function storeWebTorrentFile (video: MVideo, file: MVideoFile) { | 58 | function storeWebVideoFile (video: MVideo, file: MVideoFile) { |
59 | return storeObject({ | 59 | return storeObject({ |
60 | inputPath: VideoPathManager.Instance.getFSVideoFileOutputPath(video, file), | 60 | inputPath: VideoPathManager.Instance.getFSVideoFileOutputPath(video, file), |
61 | objectStorageKey: generateWebTorrentObjectStorageKey(file.filename), | 61 | objectStorageKey: generateWebVideoObjectStorageKey(file.filename), |
62 | bucketInfo: CONFIG.OBJECT_STORAGE.VIDEOS, | 62 | bucketInfo: CONFIG.OBJECT_STORAGE.WEB_VIDEOS, |
63 | isPrivate: video.hasPrivateStaticPath() | 63 | isPrivate: video.hasPrivateStaticPath() |
64 | }) | 64 | }) |
65 | } | 65 | } |
66 | 66 | ||
67 | // --------------------------------------------------------------------------- | 67 | // --------------------------------------------------------------------------- |
68 | 68 | ||
69 | async function updateWebTorrentFileACL (video: MVideo, file: MVideoFile) { | 69 | async function updateWebVideoFileACL (video: MVideo, file: MVideoFile) { |
70 | await updateObjectACL({ | 70 | await updateObjectACL({ |
71 | objectStorageKey: generateWebTorrentObjectStorageKey(file.filename), | 71 | objectStorageKey: generateWebVideoObjectStorageKey(file.filename), |
72 | bucketInfo: CONFIG.OBJECT_STORAGE.VIDEOS, | 72 | bucketInfo: CONFIG.OBJECT_STORAGE.WEB_VIDEOS, |
73 | isPrivate: video.hasPrivateStaticPath() | 73 | isPrivate: video.hasPrivateStaticPath() |
74 | }) | 74 | }) |
75 | } | 75 | } |
@@ -102,8 +102,8 @@ function removeHLSFileObjectStorageByFullKey (key: string) { | |||
102 | 102 | ||
103 | // --------------------------------------------------------------------------- | 103 | // --------------------------------------------------------------------------- |
104 | 104 | ||
105 | function removeWebTorrentObjectStorage (videoFile: MVideoFile) { | 105 | function removeWebVideoObjectStorage (videoFile: MVideoFile) { |
106 | return removeObject(generateWebTorrentObjectStorageKey(videoFile.filename), CONFIG.OBJECT_STORAGE.VIDEOS) | 106 | return removeObject(generateWebVideoObjectStorageKey(videoFile.filename), CONFIG.OBJECT_STORAGE.WEB_VIDEOS) |
107 | } | 107 | } |
108 | 108 | ||
109 | // --------------------------------------------------------------------------- | 109 | // --------------------------------------------------------------------------- |
@@ -122,15 +122,15 @@ async function makeHLSFileAvailable (playlist: MStreamingPlaylistVideo, filename | |||
122 | return destination | 122 | return destination |
123 | } | 123 | } |
124 | 124 | ||
125 | async function makeWebTorrentFileAvailable (filename: string, destination: string) { | 125 | async function makeWebVideoFileAvailable (filename: string, destination: string) { |
126 | const key = generateWebTorrentObjectStorageKey(filename) | 126 | const key = generateWebVideoObjectStorageKey(filename) |
127 | 127 | ||
128 | logger.info('Fetching WebTorrent file %s from object storage to %s.', key, destination, lTags()) | 128 | logger.info('Fetching Web Video file %s from object storage to %s.', key, destination, lTags()) |
129 | 129 | ||
130 | await makeAvailable({ | 130 | await makeAvailable({ |
131 | key, | 131 | key, |
132 | destination, | 132 | destination, |
133 | bucketInfo: CONFIG.OBJECT_STORAGE.VIDEOS | 133 | bucketInfo: CONFIG.OBJECT_STORAGE.WEB_VIDEOS |
134 | }) | 134 | }) |
135 | 135 | ||
136 | return destination | 136 | return destination |
@@ -138,17 +138,17 @@ async function makeWebTorrentFileAvailable (filename: string, destination: strin | |||
138 | 138 | ||
139 | // --------------------------------------------------------------------------- | 139 | // --------------------------------------------------------------------------- |
140 | 140 | ||
141 | function getWebTorrentFileReadStream (options: { | 141 | function getWebVideoFileReadStream (options: { |
142 | filename: string | 142 | filename: string |
143 | rangeHeader: string | 143 | rangeHeader: string |
144 | }) { | 144 | }) { |
145 | const { filename, rangeHeader } = options | 145 | const { filename, rangeHeader } = options |
146 | 146 | ||
147 | const key = generateWebTorrentObjectStorageKey(filename) | 147 | const key = generateWebVideoObjectStorageKey(filename) |
148 | 148 | ||
149 | return createObjectReadStream({ | 149 | return createObjectReadStream({ |
150 | key, | 150 | key, |
151 | bucketInfo: CONFIG.OBJECT_STORAGE.VIDEOS, | 151 | bucketInfo: CONFIG.OBJECT_STORAGE.WEB_VIDEOS, |
152 | rangeHeader | 152 | rangeHeader |
153 | }) | 153 | }) |
154 | } | 154 | } |
@@ -174,12 +174,12 @@ function getHLSFileReadStream (options: { | |||
174 | export { | 174 | export { |
175 | listHLSFileKeysOf, | 175 | listHLSFileKeysOf, |
176 | 176 | ||
177 | storeWebTorrentFile, | 177 | storeWebVideoFile, |
178 | storeHLSFileFromFilename, | 178 | storeHLSFileFromFilename, |
179 | storeHLSFileFromPath, | 179 | storeHLSFileFromPath, |
180 | storeHLSFileFromContent, | 180 | storeHLSFileFromContent, |
181 | 181 | ||
182 | updateWebTorrentFileACL, | 182 | updateWebVideoFileACL, |
183 | updateHLSFilesACL, | 183 | updateHLSFilesACL, |
184 | 184 | ||
185 | removeHLSObjectStorage, | 185 | removeHLSObjectStorage, |
@@ -187,11 +187,11 @@ export { | |||
187 | removeHLSFileObjectStorageByPath, | 187 | removeHLSFileObjectStorageByPath, |
188 | removeHLSFileObjectStorageByFullKey, | 188 | removeHLSFileObjectStorageByFullKey, |
189 | 189 | ||
190 | removeWebTorrentObjectStorage, | 190 | removeWebVideoObjectStorage, |
191 | 191 | ||
192 | makeWebTorrentFileAvailable, | 192 | makeWebVideoFileAvailable, |
193 | makeHLSFileAvailable, | 193 | makeHLSFileAvailable, |
194 | 194 | ||
195 | getWebTorrentFileReadStream, | 195 | getWebVideoFileReadStream, |
196 | getHLSFileReadStream | 196 | getHLSFileReadStream |
197 | } | 197 | } |
diff --git a/server/lib/paths.ts b/server/lib/paths.ts index 470970f55..db1cdede2 100644 --- a/server/lib/paths.ts +++ b/server/lib/paths.ts | |||
@@ -8,7 +8,7 @@ import { isVideoInPrivateDirectory } from './video-privacy' | |||
8 | 8 | ||
9 | // ################## Video file name ################## | 9 | // ################## Video file name ################## |
10 | 10 | ||
11 | function generateWebTorrentVideoFilename (resolution: number, extname: string) { | 11 | function generateWebVideoFilename (resolution: number, extname: string) { |
12 | return buildUUID() + '-' + resolution + extname | 12 | return buildUUID() + '-' + resolution + extname |
13 | } | 13 | } |
14 | 14 | ||
@@ -76,7 +76,7 @@ function getFSTorrentFilePath (videoFile: MVideoFile) { | |||
76 | 76 | ||
77 | export { | 77 | export { |
78 | generateHLSVideoFilename, | 78 | generateHLSVideoFilename, |
79 | generateWebTorrentVideoFilename, | 79 | generateWebVideoFilename, |
80 | 80 | ||
81 | generateTorrentFileName, | 81 | generateTorrentFileName, |
82 | getFSTorrentFilePath, | 82 | getFSTorrentFilePath, |
diff --git a/server/lib/plugins/plugin-helpers-builder.ts b/server/lib/plugins/plugin-helpers-builder.ts index d235f52c0..b4e3eece4 100644 --- a/server/lib/plugins/plugin-helpers-builder.ts +++ b/server/lib/plugins/plugin-helpers-builder.ts | |||
@@ -104,7 +104,7 @@ function buildVideosHelpers () { | |||
104 | const video = await VideoModel.loadFull(id) | 104 | const video = await VideoModel.loadFull(id) |
105 | if (!video) return undefined | 105 | if (!video) return undefined |
106 | 106 | ||
107 | const webtorrentVideoFiles = (video.VideoFiles || []).map(f => ({ | 107 | const webVideoFiles = (video.VideoFiles || []).map(f => ({ |
108 | path: f.storage === VideoStorage.FILE_SYSTEM | 108 | path: f.storage === VideoStorage.FILE_SYSTEM |
109 | ? VideoPathManager.Instance.getFSVideoFileOutputPath(video, f) | 109 | ? VideoPathManager.Instance.getFSVideoFileOutputPath(video, f) |
110 | : null, | 110 | : null, |
@@ -138,8 +138,12 @@ function buildVideosHelpers () { | |||
138 | })) | 138 | })) |
139 | 139 | ||
140 | return { | 140 | return { |
141 | webtorrent: { | 141 | webtorrent: { // TODO: remove in v7 |
142 | videoFiles: webtorrentVideoFiles | 142 | videoFiles: webVideoFiles |
143 | }, | ||
144 | |||
145 | webVideo: { | ||
146 | videoFiles: webVideoFiles | ||
143 | }, | 147 | }, |
144 | 148 | ||
145 | hls: { | 149 | hls: { |
diff --git a/server/lib/redis.ts b/server/lib/redis.ts index 8430b2227..48d9986b5 100644 --- a/server/lib/redis.ts +++ b/server/lib/redis.ts | |||
@@ -325,8 +325,8 @@ class Redis { | |||
325 | const value = await this.getValue('resumable-upload-' + uploadId) | 325 | const value = await this.getValue('resumable-upload-' + uploadId) |
326 | 326 | ||
327 | return value | 327 | return value |
328 | ? JSON.parse(value) | 328 | ? JSON.parse(value) as { video: { id: number, shortUUID: string, uuid: string } } |
329 | : '' | 329 | : undefined |
330 | } | 330 | } |
331 | 331 | ||
332 | deleteUploadSession (uploadId: string) { | 332 | deleteUploadSession (uploadId: string) { |
diff --git a/server/lib/runners/job-handlers/shared/vod-helpers.ts b/server/lib/runners/job-handlers/shared/vod-helpers.ts index 93ae89ff8..1a2ad02ca 100644 --- a/server/lib/runners/job-handlers/shared/vod-helpers.ts +++ b/server/lib/runners/job-handlers/shared/vod-helpers.ts | |||
@@ -2,7 +2,7 @@ import { move } from 'fs-extra' | |||
2 | import { dirname, join } from 'path' | 2 | import { dirname, join } from 'path' |
3 | import { logger, LoggerTagsFn } from '@server/helpers/logger' | 3 | import { logger, LoggerTagsFn } from '@server/helpers/logger' |
4 | import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding' | 4 | import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding' |
5 | import { onWebTorrentVideoFileTranscoding } from '@server/lib/transcoding/web-transcoding' | 5 | import { onWebVideoFileTranscoding } from '@server/lib/transcoding/web-transcoding' |
6 | import { buildNewFile } from '@server/lib/video-file' | 6 | import { buildNewFile } from '@server/lib/video-file' |
7 | import { VideoModel } from '@server/models/video/video' | 7 | import { VideoModel } from '@server/models/video/video' |
8 | import { MVideoFullLight } from '@server/types/models' | 8 | import { MVideoFullLight } from '@server/types/models' |
@@ -22,7 +22,7 @@ export async function onVODWebVideoOrAudioMergeTranscodingJob (options: { | |||
22 | const newVideoFilePath = join(dirname(videoFilePath), videoFile.filename) | 22 | const newVideoFilePath = join(dirname(videoFilePath), videoFile.filename) |
23 | await move(videoFilePath, newVideoFilePath) | 23 | await move(videoFilePath, newVideoFilePath) |
24 | 24 | ||
25 | await onWebTorrentVideoFileTranscoding({ | 25 | await onWebVideoFileTranscoding({ |
26 | video, | 26 | video, |
27 | videoFile, | 27 | videoFile, |
28 | videoOutputPath: newVideoFilePath | 28 | videoOutputPath: newVideoFilePath |
diff --git a/server/lib/runners/job-handlers/vod-audio-merge-transcoding-job-handler.ts b/server/lib/runners/job-handlers/vod-audio-merge-transcoding-job-handler.ts index 5f247d792..905007db9 100644 --- a/server/lib/runners/job-handlers/vod-audio-merge-transcoding-job-handler.ts +++ b/server/lib/runners/job-handlers/vod-audio-merge-transcoding-job-handler.ts | |||
@@ -83,7 +83,7 @@ export class VODAudioMergeTranscodingJobHandler extends AbstractVODTranscodingJo | |||
83 | 83 | ||
84 | // We can remove the old audio file | 84 | // We can remove the old audio file |
85 | const oldAudioFile = video.VideoFiles[0] | 85 | const oldAudioFile = video.VideoFiles[0] |
86 | await video.removeWebTorrentFile(oldAudioFile) | 86 | await video.removeWebVideoFile(oldAudioFile) |
87 | await oldAudioFile.destroy() | 87 | await oldAudioFile.destroy() |
88 | video.VideoFiles = [] | 88 | video.VideoFiles = [] |
89 | 89 | ||
diff --git a/server/lib/runners/job-handlers/vod-hls-transcoding-job-handler.ts b/server/lib/runners/job-handlers/vod-hls-transcoding-job-handler.ts index cc94bcbda..02845952c 100644 --- a/server/lib/runners/job-handlers/vod-hls-transcoding-job-handler.ts +++ b/server/lib/runners/job-handlers/vod-hls-transcoding-job-handler.ts | |||
@@ -5,7 +5,7 @@ import { renameVideoFileInPlaylist } from '@server/lib/hls' | |||
5 | import { getHlsResolutionPlaylistFilename } from '@server/lib/paths' | 5 | import { getHlsResolutionPlaylistFilename } from '@server/lib/paths' |
6 | import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding' | 6 | import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding' |
7 | import { onHLSVideoFileTranscoding } from '@server/lib/transcoding/hls-transcoding' | 7 | import { onHLSVideoFileTranscoding } from '@server/lib/transcoding/hls-transcoding' |
8 | import { buildNewFile, removeAllWebTorrentFiles } from '@server/lib/video-file' | 8 | import { buildNewFile, removeAllWebVideoFiles } from '@server/lib/video-file' |
9 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' | 9 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' |
10 | import { MVideo } from '@server/types/models' | 10 | import { MVideo } from '@server/types/models' |
11 | import { MRunnerJob } from '@server/types/models/runners' | 11 | import { MRunnerJob } from '@server/types/models/runners' |
@@ -106,7 +106,7 @@ export class VODHLSTranscodingJobHandler extends AbstractVODTranscodingJobHandle | |||
106 | if (privatePayload.deleteWebVideoFiles === true) { | 106 | if (privatePayload.deleteWebVideoFiles === true) { |
107 | logger.info('Removing web video files of %s now we have a HLS version of it.', video.uuid, this.lTags(video.uuid)) | 107 | logger.info('Removing web video files of %s now we have a HLS version of it.', video.uuid, this.lTags(video.uuid)) |
108 | 108 | ||
109 | await removeAllWebTorrentFiles(video) | 109 | await removeAllWebVideoFiles(video) |
110 | } | 110 | } |
111 | 111 | ||
112 | logger.info('Runner VOD HLS job %s for %s ended.', runnerJob.uuid, video.uuid, this.lTags(runnerJob.uuid, video.uuid)) | 112 | logger.info('Runner VOD HLS job %s for %s ended.', runnerJob.uuid, video.uuid, this.lTags(runnerJob.uuid, video.uuid)) |
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts index dc450c338..24d340a73 100644 --- a/server/lib/schedulers/videos-redundancy-scheduler.ts +++ b/server/lib/schedulers/videos-redundancy-scheduler.ts | |||
@@ -23,7 +23,7 @@ import { getLocalVideoCacheFileActivityPubUrl, getLocalVideoCacheStreamingPlayli | |||
23 | import { getOrCreateAPVideo } from '../activitypub/videos' | 23 | import { getOrCreateAPVideo } from '../activitypub/videos' |
24 | import { downloadPlaylistSegments } from '../hls' | 24 | import { downloadPlaylistSegments } from '../hls' |
25 | import { removeVideoRedundancy } from '../redundancy' | 25 | import { removeVideoRedundancy } from '../redundancy' |
26 | import { generateHLSRedundancyUrl, generateWebTorrentRedundancyUrl } from '../video-urls' | 26 | import { generateHLSRedundancyUrl, generateWebVideoRedundancyUrl } from '../video-urls' |
27 | import { AbstractScheduler } from './abstract-scheduler' | 27 | import { AbstractScheduler } from './abstract-scheduler' |
28 | 28 | ||
29 | const lTags = loggerTagsFactory('redundancy') | 29 | const lTags = loggerTagsFactory('redundancy') |
@@ -244,7 +244,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
244 | const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({ | 244 | const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({ |
245 | expiresOn, | 245 | expiresOn, |
246 | url: getLocalVideoCacheFileActivityPubUrl(file), | 246 | url: getLocalVideoCacheFileActivityPubUrl(file), |
247 | fileUrl: generateWebTorrentRedundancyUrl(file), | 247 | fileUrl: generateWebVideoRedundancyUrl(file), |
248 | strategy, | 248 | strategy, |
249 | videoFileId: file.id, | 249 | videoFileId: file.id, |
250 | actorId: serverActor.id | 250 | actorId: serverActor.id |
diff --git a/server/lib/server-config-manager.ts b/server/lib/server-config-manager.ts index 924adb337..5ce89b16d 100644 --- a/server/lib/server-config-manager.ts +++ b/server/lib/server-config-manager.ts | |||
@@ -132,8 +132,8 @@ class ServerConfigManager { | |||
132 | hls: { | 132 | hls: { |
133 | enabled: CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.HLS.ENABLED | 133 | enabled: CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.HLS.ENABLED |
134 | }, | 134 | }, |
135 | webtorrent: { | 135 | web_videos: { |
136 | enabled: CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.WEBTORRENT.ENABLED | 136 | enabled: CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED |
137 | }, | 137 | }, |
138 | enabledResolutions: this.getEnabledResolutions('vod'), | 138 | enabledResolutions: this.getEnabledResolutions('vod'), |
139 | profile: CONFIG.TRANSCODING.PROFILE, | 139 | profile: CONFIG.TRANSCODING.PROFILE, |
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index 02b867a91..d95442795 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts | |||
@@ -7,13 +7,12 @@ import { ThumbnailModel } from '../models/video/thumbnail' | |||
7 | import { MVideoFile, MVideoThumbnail, MVideoUUID } from '../types/models' | 7 | import { MVideoFile, MVideoThumbnail, MVideoUUID } from '../types/models' |
8 | import { MThumbnail } from '../types/models/video/thumbnail' | 8 | import { MThumbnail } from '../types/models/video/thumbnail' |
9 | import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' | 9 | import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' |
10 | import { downloadImageFromWorker } from './local-actor' | ||
11 | import { VideoPathManager } from './video-path-manager' | 10 | import { VideoPathManager } from './video-path-manager' |
12 | import { processImageFromWorker } from './worker/parent-process' | 11 | import { downloadImageFromWorker, processImageFromWorker } from './worker/parent-process' |
13 | 12 | ||
14 | type ImageSize = { height?: number, width?: number } | 13 | type ImageSize = { height?: number, width?: number } |
15 | 14 | ||
16 | function updatePlaylistMiniatureFromExisting (options: { | 15 | function updateLocalPlaylistMiniatureFromExisting (options: { |
17 | inputPath: string | 16 | inputPath: string |
18 | playlist: MVideoPlaylistThumbnail | 17 | playlist: MVideoPlaylistThumbnail |
19 | automaticallyGenerated: boolean | 18 | automaticallyGenerated: boolean |
@@ -35,11 +34,12 @@ function updatePlaylistMiniatureFromExisting (options: { | |||
35 | width, | 34 | width, |
36 | type, | 35 | type, |
37 | automaticallyGenerated, | 36 | automaticallyGenerated, |
37 | onDisk: true, | ||
38 | existingThumbnail | 38 | existingThumbnail |
39 | }) | 39 | }) |
40 | } | 40 | } |
41 | 41 | ||
42 | function updatePlaylistMiniatureFromUrl (options: { | 42 | function updateRemotePlaylistMiniatureFromUrl (options: { |
43 | downloadUrl: string | 43 | downloadUrl: string |
44 | playlist: MVideoPlaylistThumbnail | 44 | playlist: MVideoPlaylistThumbnail |
45 | size?: ImageSize | 45 | size?: ImageSize |
@@ -57,42 +57,10 @@ function updatePlaylistMiniatureFromUrl (options: { | |||
57 | return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } }) | 57 | return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } }) |
58 | } | 58 | } |
59 | 59 | ||
60 | return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) | 60 | return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl, onDisk: true }) |
61 | } | 61 | } |
62 | 62 | ||
63 | function updateVideoMiniatureFromUrl (options: { | 63 | function updateLocalVideoMiniatureFromExisting (options: { |
64 | downloadUrl: string | ||
65 | video: MVideoThumbnail | ||
66 | type: ThumbnailType | ||
67 | size?: ImageSize | ||
68 | }) { | ||
69 | const { downloadUrl, video, type, size } = options | ||
70 | const { filename: updatedFilename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) | ||
71 | |||
72 | // Only save the file URL if it is a remote video | ||
73 | const fileUrl = video.isOwned() | ||
74 | ? null | ||
75 | : downloadUrl | ||
76 | |||
77 | const thumbnailUrlChanged = hasThumbnailUrlChanged(existingThumbnail, downloadUrl, video) | ||
78 | |||
79 | // Do not change the thumbnail filename if the file did not change | ||
80 | const filename = thumbnailUrlChanged | ||
81 | ? updatedFilename | ||
82 | : existingThumbnail.filename | ||
83 | |||
84 | const thumbnailCreator = () => { | ||
85 | if (thumbnailUrlChanged) { | ||
86 | return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } }) | ||
87 | } | ||
88 | |||
89 | return Promise.resolve() | ||
90 | } | ||
91 | |||
92 | return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) | ||
93 | } | ||
94 | |||
95 | function updateVideoMiniatureFromExisting (options: { | ||
96 | inputPath: string | 64 | inputPath: string |
97 | video: MVideoThumbnail | 65 | video: MVideoThumbnail |
98 | type: ThumbnailType | 66 | type: ThumbnailType |
@@ -115,11 +83,12 @@ function updateVideoMiniatureFromExisting (options: { | |||
115 | width, | 83 | width, |
116 | type, | 84 | type, |
117 | automaticallyGenerated, | 85 | automaticallyGenerated, |
118 | existingThumbnail | 86 | existingThumbnail, |
87 | onDisk: true | ||
119 | }) | 88 | }) |
120 | } | 89 | } |
121 | 90 | ||
122 | function generateVideoMiniature (options: { | 91 | function generateLocalVideoMiniature (options: { |
123 | video: MVideoThumbnail | 92 | video: MVideoThumbnail |
124 | videoFile: MVideoFile | 93 | videoFile: MVideoFile |
125 | type: ThumbnailType | 94 | type: ThumbnailType |
@@ -150,34 +119,68 @@ function generateVideoMiniature (options: { | |||
150 | width, | 119 | width, |
151 | type, | 120 | type, |
152 | automaticallyGenerated: true, | 121 | automaticallyGenerated: true, |
122 | onDisk: true, | ||
153 | existingThumbnail | 123 | existingThumbnail |
154 | }) | 124 | }) |
155 | }) | 125 | }) |
156 | } | 126 | } |
157 | 127 | ||
158 | function updatePlaceholderThumbnail (options: { | 128 | // --------------------------------------------------------------------------- |
159 | fileUrl: string | 129 | |
130 | function updateLocalVideoMiniatureFromUrl (options: { | ||
131 | downloadUrl: string | ||
160 | video: MVideoThumbnail | 132 | video: MVideoThumbnail |
161 | type: ThumbnailType | 133 | type: ThumbnailType |
162 | size: ImageSize | 134 | size?: ImageSize |
163 | }) { | 135 | }) { |
164 | const { fileUrl, video, type, size } = options | 136 | const { downloadUrl, video, type, size } = options |
165 | const { filename: updatedFilename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) | 137 | const { filename: updatedFilename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) |
166 | 138 | ||
167 | const thumbnailUrlChanged = hasThumbnailUrlChanged(existingThumbnail, fileUrl, video) | 139 | // Only save the file URL if it is a remote video |
140 | const fileUrl = video.isOwned() | ||
141 | ? null | ||
142 | : downloadUrl | ||
168 | 143 | ||
169 | const thumbnail = existingThumbnail || new ThumbnailModel() | 144 | const thumbnailUrlChanged = hasThumbnailUrlChanged(existingThumbnail, downloadUrl, video) |
170 | 145 | ||
171 | // Do not change the thumbnail filename if the file did not change | 146 | // Do not change the thumbnail filename if the file did not change |
172 | const filename = thumbnailUrlChanged | 147 | const filename = thumbnailUrlChanged |
173 | ? updatedFilename | 148 | ? updatedFilename |
174 | : existingThumbnail.filename | 149 | : existingThumbnail.filename |
175 | 150 | ||
176 | thumbnail.filename = filename | 151 | const thumbnailCreator = () => { |
152 | if (thumbnailUrlChanged) { | ||
153 | return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } }) | ||
154 | } | ||
155 | |||
156 | return Promise.resolve() | ||
157 | } | ||
158 | |||
159 | return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl, onDisk: true }) | ||
160 | } | ||
161 | |||
162 | function updateRemoteVideoThumbnail (options: { | ||
163 | fileUrl: string | ||
164 | video: MVideoThumbnail | ||
165 | type: ThumbnailType | ||
166 | size: ImageSize | ||
167 | onDisk: boolean | ||
168 | }) { | ||
169 | const { fileUrl, video, type, size, onDisk } = options | ||
170 | const { filename: generatedFilename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) | ||
171 | |||
172 | const thumbnail = existingThumbnail || new ThumbnailModel() | ||
173 | |||
174 | // Do not change the thumbnail filename if the file did not change | ||
175 | if (hasThumbnailUrlChanged(existingThumbnail, fileUrl, video)) { | ||
176 | thumbnail.filename = generatedFilename | ||
177 | } | ||
178 | |||
177 | thumbnail.height = height | 179 | thumbnail.height = height |
178 | thumbnail.width = width | 180 | thumbnail.width = width |
179 | thumbnail.type = type | 181 | thumbnail.type = type |
180 | thumbnail.fileUrl = fileUrl | 182 | thumbnail.fileUrl = fileUrl |
183 | thumbnail.onDisk = onDisk | ||
181 | 184 | ||
182 | return thumbnail | 185 | return thumbnail |
183 | } | 186 | } |
@@ -185,14 +188,18 @@ function updatePlaceholderThumbnail (options: { | |||
185 | // --------------------------------------------------------------------------- | 188 | // --------------------------------------------------------------------------- |
186 | 189 | ||
187 | export { | 190 | export { |
188 | generateVideoMiniature, | 191 | generateLocalVideoMiniature, |
189 | updateVideoMiniatureFromUrl, | 192 | updateLocalVideoMiniatureFromUrl, |
190 | updateVideoMiniatureFromExisting, | 193 | updateLocalVideoMiniatureFromExisting, |
191 | updatePlaceholderThumbnail, | 194 | updateRemoteVideoThumbnail, |
192 | updatePlaylistMiniatureFromUrl, | 195 | updateRemotePlaylistMiniatureFromUrl, |
193 | updatePlaylistMiniatureFromExisting | 196 | updateLocalPlaylistMiniatureFromExisting |
194 | } | 197 | } |
195 | 198 | ||
199 | // --------------------------------------------------------------------------- | ||
200 | // Private | ||
201 | // --------------------------------------------------------------------------- | ||
202 | |||
196 | function hasThumbnailUrlChanged (existingThumbnail: MThumbnail, downloadUrl: string, video: MVideoUUID) { | 203 | function hasThumbnailUrlChanged (existingThumbnail: MThumbnail, downloadUrl: string, video: MVideoUUID) { |
197 | const existingUrl = existingThumbnail | 204 | const existingUrl = existingThumbnail |
198 | ? existingThumbnail.fileUrl | 205 | ? existingThumbnail.fileUrl |
@@ -258,6 +265,7 @@ async function updateThumbnailFromFunction (parameters: { | |||
258 | height: number | 265 | height: number |
259 | width: number | 266 | width: number |
260 | type: ThumbnailType | 267 | type: ThumbnailType |
268 | onDisk: boolean | ||
261 | automaticallyGenerated?: boolean | 269 | automaticallyGenerated?: boolean |
262 | fileUrl?: string | 270 | fileUrl?: string |
263 | existingThumbnail?: MThumbnail | 271 | existingThumbnail?: MThumbnail |
@@ -269,6 +277,7 @@ async function updateThumbnailFromFunction (parameters: { | |||
269 | height, | 277 | height, |
270 | type, | 278 | type, |
271 | existingThumbnail, | 279 | existingThumbnail, |
280 | onDisk, | ||
272 | automaticallyGenerated = null, | 281 | automaticallyGenerated = null, |
273 | fileUrl = null | 282 | fileUrl = null |
274 | } = parameters | 283 | } = parameters |
@@ -285,6 +294,7 @@ async function updateThumbnailFromFunction (parameters: { | |||
285 | thumbnail.type = type | 294 | thumbnail.type = type |
286 | thumbnail.fileUrl = fileUrl | 295 | thumbnail.fileUrl = fileUrl |
287 | thumbnail.automaticallyGenerated = automaticallyGenerated | 296 | thumbnail.automaticallyGenerated = automaticallyGenerated |
297 | thumbnail.onDisk = onDisk | ||
288 | 298 | ||
289 | if (oldFilename) thumbnail.previousThumbnailFilename = oldFilename | 299 | if (oldFilename) thumbnail.previousThumbnailFilename = oldFilename |
290 | 300 | ||
diff --git a/server/lib/transcoding/create-transcoding-job.ts b/server/lib/transcoding/create-transcoding-job.ts index abe32684d..d78e68b87 100644 --- a/server/lib/transcoding/create-transcoding-job.ts +++ b/server/lib/transcoding/create-transcoding-job.ts | |||
@@ -15,7 +15,7 @@ export function createOptimizeOrMergeAudioJobs (options: { | |||
15 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
16 | 16 | ||
17 | export function createTranscodingJobs (options: { | 17 | export function createTranscodingJobs (options: { |
18 | transcodingType: 'hls' | 'webtorrent' | 18 | transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7 |
19 | video: MVideoFullLight | 19 | video: MVideoFullLight |
20 | resolutions: number[] | 20 | resolutions: number[] |
21 | isNewVideo: boolean | 21 | isNewVideo: boolean |
diff --git a/server/lib/transcoding/shared/job-builders/abstract-job-builder.ts b/server/lib/transcoding/shared/job-builders/abstract-job-builder.ts index 80dc05bfb..15fc814ae 100644 --- a/server/lib/transcoding/shared/job-builders/abstract-job-builder.ts +++ b/server/lib/transcoding/shared/job-builders/abstract-job-builder.ts | |||
@@ -12,7 +12,7 @@ export abstract class AbstractJobBuilder { | |||
12 | }): Promise<any> | 12 | }): Promise<any> |
13 | 13 | ||
14 | abstract createTranscodingJobs (options: { | 14 | abstract createTranscodingJobs (options: { |
15 | transcodingType: 'hls' | 'webtorrent' | 15 | transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7 |
16 | video: MVideoFullLight | 16 | video: MVideoFullLight |
17 | resolutions: number[] | 17 | resolutions: number[] |
18 | isNewVideo: boolean | 18 | isNewVideo: boolean |
diff --git a/server/lib/transcoding/shared/job-builders/transcoding-job-queue-builder.ts b/server/lib/transcoding/shared/job-builders/transcoding-job-queue-builder.ts index 4f802e2a6..0505c2b2f 100644 --- a/server/lib/transcoding/shared/job-builders/transcoding-job-queue-builder.ts +++ b/server/lib/transcoding/shared/job-builders/transcoding-job-queue-builder.ts | |||
@@ -12,7 +12,7 @@ import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAud | |||
12 | import { | 12 | import { |
13 | HLSTranscodingPayload, | 13 | HLSTranscodingPayload, |
14 | MergeAudioTranscodingPayload, | 14 | MergeAudioTranscodingPayload, |
15 | NewWebTorrentResolutionTranscodingPayload, | 15 | NewWebVideoResolutionTranscodingPayload, |
16 | OptimizeTranscodingPayload, | 16 | OptimizeTranscodingPayload, |
17 | VideoTranscodingPayload | 17 | VideoTranscodingPayload |
18 | } from '@shared/models' | 18 | } from '@shared/models' |
@@ -33,7 +33,7 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
33 | const { video, videoFile, isNewVideo, user, videoFileAlreadyLocked } = options | 33 | const { video, videoFile, isNewVideo, user, videoFileAlreadyLocked } = options |
34 | 34 | ||
35 | let mergeOrOptimizePayload: MergeAudioTranscodingPayload | OptimizeTranscodingPayload | 35 | let mergeOrOptimizePayload: MergeAudioTranscodingPayload | OptimizeTranscodingPayload |
36 | let nextTranscodingSequentialJobPayloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[][] = [] | 36 | let nextTranscodingSequentialJobPayloads: (NewWebVideoResolutionTranscodingPayload | HLSTranscodingPayload)[][] = [] |
37 | 37 | ||
38 | const mutexReleaser = videoFileAlreadyLocked | 38 | const mutexReleaser = videoFileAlreadyLocked |
39 | ? () => {} | 39 | ? () => {} |
@@ -60,7 +60,7 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
60 | if (CONFIG.TRANSCODING.HLS.ENABLED === true) { | 60 | if (CONFIG.TRANSCODING.HLS.ENABLED === true) { |
61 | nextTranscodingSequentialJobPayloads.push([ | 61 | nextTranscodingSequentialJobPayloads.push([ |
62 | this.buildHLSJobPayload({ | 62 | this.buildHLSJobPayload({ |
63 | deleteWebTorrentFiles: CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false, | 63 | deleteWebVideoFiles: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED === false, |
64 | 64 | ||
65 | // We had some issues with a web video quick transcoded while producing a HLS version of it | 65 | // We had some issues with a web video quick transcoded while producing a HLS version of it |
66 | copyCodecs: !quickTranscode, | 66 | copyCodecs: !quickTranscode, |
@@ -116,7 +116,7 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
116 | // --------------------------------------------------------------------------- | 116 | // --------------------------------------------------------------------------- |
117 | 117 | ||
118 | async createTranscodingJobs (options: { | 118 | async createTranscodingJobs (options: { |
119 | transcodingType: 'hls' | 'webtorrent' | 119 | transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7 |
120 | video: MVideoFullLight | 120 | video: MVideoFullLight |
121 | resolutions: number[] | 121 | resolutions: number[] |
122 | isNewVideo: boolean | 122 | isNewVideo: boolean |
@@ -138,8 +138,8 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
138 | return this.buildHLSJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo }) | 138 | return this.buildHLSJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo }) |
139 | } | 139 | } |
140 | 140 | ||
141 | if (transcodingType === 'webtorrent') { | 141 | if (transcodingType === 'webtorrent' || transcodingType === 'web-video') { |
142 | return this.buildWebTorrentJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo }) | 142 | return this.buildWebVideoJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo }) |
143 | } | 143 | } |
144 | 144 | ||
145 | throw new Error('Unknown transcoding type') | 145 | throw new Error('Unknown transcoding type') |
@@ -149,7 +149,7 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
149 | 149 | ||
150 | const parent = transcodingType === 'hls' | 150 | const parent = transcodingType === 'hls' |
151 | ? this.buildHLSJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo }) | 151 | ? this.buildHLSJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo }) |
152 | : this.buildWebTorrentJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo }) | 152 | : this.buildWebVideoJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo }) |
153 | 153 | ||
154 | // Process the last resolution after the other ones to prevent concurrency issue | 154 | // Process the last resolution after the other ones to prevent concurrency issue |
155 | // Because low resolutions use the biggest one as ffmpeg input | 155 | // Because low resolutions use the biggest one as ffmpeg input |
@@ -160,8 +160,8 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
160 | 160 | ||
161 | private async createTranscodingJobsWithChildren (options: { | 161 | private async createTranscodingJobsWithChildren (options: { |
162 | videoUUID: string | 162 | videoUUID: string |
163 | parent: (HLSTranscodingPayload | NewWebTorrentResolutionTranscodingPayload) | 163 | parent: (HLSTranscodingPayload | NewWebVideoResolutionTranscodingPayload) |
164 | children: (HLSTranscodingPayload | NewWebTorrentResolutionTranscodingPayload)[] | 164 | children: (HLSTranscodingPayload | NewWebVideoResolutionTranscodingPayload)[] |
165 | user: MUserId | null | 165 | user: MUserId | null |
166 | }) { | 166 | }) { |
167 | const { videoUUID, parent, children, user } = options | 167 | const { videoUUID, parent, children, user } = options |
@@ -203,14 +203,14 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
203 | options | 203 | options |
204 | ) | 204 | ) |
205 | 205 | ||
206 | const sequentialPayloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[][] = [] | 206 | const sequentialPayloads: (NewWebVideoResolutionTranscodingPayload | HLSTranscodingPayload)[][] = [] |
207 | 207 | ||
208 | for (const resolution of resolutionsEnabled) { | 208 | for (const resolution of resolutionsEnabled) { |
209 | const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution }) | 209 | const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution }) |
210 | 210 | ||
211 | if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) { | 211 | if (CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED) { |
212 | const payloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[] = [ | 212 | const payloads: (NewWebVideoResolutionTranscodingPayload | HLSTranscodingPayload)[] = [ |
213 | this.buildWebTorrentJobPayload({ | 213 | this.buildWebVideoJobPayload({ |
214 | videoUUID: video.uuid, | 214 | videoUUID: video.uuid, |
215 | resolution, | 215 | resolution, |
216 | fps, | 216 | fps, |
@@ -253,10 +253,10 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
253 | resolution: number | 253 | resolution: number |
254 | fps: number | 254 | fps: number |
255 | isNewVideo: boolean | 255 | isNewVideo: boolean |
256 | deleteWebTorrentFiles?: boolean // default false | 256 | deleteWebVideoFiles?: boolean // default false |
257 | copyCodecs?: boolean // default false | 257 | copyCodecs?: boolean // default false |
258 | }): HLSTranscodingPayload { | 258 | }): HLSTranscodingPayload { |
259 | const { videoUUID, resolution, fps, isNewVideo, deleteWebTorrentFiles = false, copyCodecs = false } = options | 259 | const { videoUUID, resolution, fps, isNewVideo, deleteWebVideoFiles = false, copyCodecs = false } = options |
260 | 260 | ||
261 | return { | 261 | return { |
262 | type: 'new-resolution-to-hls', | 262 | type: 'new-resolution-to-hls', |
@@ -265,20 +265,20 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
265 | fps, | 265 | fps, |
266 | copyCodecs, | 266 | copyCodecs, |
267 | isNewVideo, | 267 | isNewVideo, |
268 | deleteWebTorrentFiles | 268 | deleteWebVideoFiles |
269 | } | 269 | } |
270 | } | 270 | } |
271 | 271 | ||
272 | private buildWebTorrentJobPayload (options: { | 272 | private buildWebVideoJobPayload (options: { |
273 | videoUUID: string | 273 | videoUUID: string |
274 | resolution: number | 274 | resolution: number |
275 | fps: number | 275 | fps: number |
276 | isNewVideo: boolean | 276 | isNewVideo: boolean |
277 | }): NewWebTorrentResolutionTranscodingPayload { | 277 | }): NewWebVideoResolutionTranscodingPayload { |
278 | const { videoUUID, resolution, fps, isNewVideo } = options | 278 | const { videoUUID, resolution, fps, isNewVideo } = options |
279 | 279 | ||
280 | return { | 280 | return { |
281 | type: 'new-resolution-to-webtorrent', | 281 | type: 'new-resolution-to-web-video', |
282 | videoUUID, | 282 | videoUUID, |
283 | isNewVideo, | 283 | isNewVideo, |
284 | resolution, | 284 | resolution, |
@@ -294,7 +294,7 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
294 | const { videoUUID, isNewVideo, hasChildren } = options | 294 | const { videoUUID, isNewVideo, hasChildren } = options |
295 | 295 | ||
296 | return { | 296 | return { |
297 | type: 'merge-audio-to-webtorrent', | 297 | type: 'merge-audio-to-web-video', |
298 | resolution: DEFAULT_AUDIO_RESOLUTION, | 298 | resolution: DEFAULT_AUDIO_RESOLUTION, |
299 | fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE, | 299 | fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE, |
300 | videoUUID, | 300 | videoUUID, |
@@ -312,7 +312,7 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
312 | const { videoUUID, quickTranscode, isNewVideo, hasChildren } = options | 312 | const { videoUUID, quickTranscode, isNewVideo, hasChildren } = options |
313 | 313 | ||
314 | return { | 314 | return { |
315 | type: 'optimize-to-webtorrent', | 315 | type: 'optimize-to-web-video', |
316 | videoUUID, | 316 | videoUUID, |
317 | isNewVideo, | 317 | isNewVideo, |
318 | hasChildren, | 318 | hasChildren, |
diff --git a/server/lib/transcoding/shared/job-builders/transcoding-runner-job-builder.ts b/server/lib/transcoding/shared/job-builders/transcoding-runner-job-builder.ts index ba2a46f44..f0671bd7a 100644 --- a/server/lib/transcoding/shared/job-builders/transcoding-runner-job-builder.ts +++ b/server/lib/transcoding/shared/job-builders/transcoding-runner-job-builder.ts | |||
@@ -62,7 +62,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder { | |||
62 | if (CONFIG.TRANSCODING.HLS.ENABLED === true) { | 62 | if (CONFIG.TRANSCODING.HLS.ENABLED === true) { |
63 | await new VODHLSTranscodingJobHandler().create({ | 63 | await new VODHLSTranscodingJobHandler().create({ |
64 | video, | 64 | video, |
65 | deleteWebVideoFiles: CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false, | 65 | deleteWebVideoFiles: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED === false, |
66 | resolution: maxResolution, | 66 | resolution: maxResolution, |
67 | fps, | 67 | fps, |
68 | isNewVideo, | 68 | isNewVideo, |
@@ -89,7 +89,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder { | |||
89 | // --------------------------------------------------------------------------- | 89 | // --------------------------------------------------------------------------- |
90 | 90 | ||
91 | async createTranscodingJobs (options: { | 91 | async createTranscodingJobs (options: { |
92 | transcodingType: 'hls' | 'webtorrent' | 92 | transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7 |
93 | video: MVideoFullLight | 93 | video: MVideoFullLight |
94 | resolutions: number[] | 94 | resolutions: number[] |
95 | isNewVideo: boolean | 95 | isNewVideo: boolean |
@@ -130,7 +130,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder { | |||
130 | continue | 130 | continue |
131 | } | 131 | } |
132 | 132 | ||
133 | if (transcodingType === 'webtorrent') { | 133 | if (transcodingType === 'webtorrent' || transcodingType === 'web-video') { |
134 | await new VODWebVideoTranscodingJobHandler().create({ | 134 | await new VODWebVideoTranscodingJobHandler().create({ |
135 | video, | 135 | video, |
136 | resolution, | 136 | resolution, |
@@ -169,7 +169,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder { | |||
169 | for (const resolution of resolutionsEnabled) { | 169 | for (const resolution of resolutionsEnabled) { |
170 | const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution }) | 170 | const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution }) |
171 | 171 | ||
172 | if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) { | 172 | if (CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED) { |
173 | await new VODWebVideoTranscodingJobHandler().create({ | 173 | await new VODWebVideoTranscodingJobHandler().create({ |
174 | video, | 174 | video, |
175 | resolution, | 175 | resolution, |
diff --git a/server/lib/transcoding/web-transcoding.ts b/server/lib/transcoding/web-transcoding.ts index 7cc8f20bc..f92d457a0 100644 --- a/server/lib/transcoding/web-transcoding.ts +++ b/server/lib/transcoding/web-transcoding.ts | |||
@@ -9,7 +9,8 @@ import { ffprobePromise, getVideoStreamDuration, getVideoStreamFPS, TranscodeVOD | |||
9 | import { VideoResolution, VideoStorage } from '@shared/models' | 9 | import { VideoResolution, VideoStorage } from '@shared/models' |
10 | import { CONFIG } from '../../initializers/config' | 10 | import { CONFIG } from '../../initializers/config' |
11 | import { VideoFileModel } from '../../models/video/video-file' | 11 | import { VideoFileModel } from '../../models/video/video-file' |
12 | import { generateWebTorrentVideoFilename } from '../paths' | 12 | import { JobQueue } from '../job-queue' |
13 | import { generateWebVideoFilename } from '../paths' | ||
13 | import { buildFileMetadata } from '../video-file' | 14 | import { buildFileMetadata } from '../video-file' |
14 | import { VideoPathManager } from '../video-path-manager' | 15 | import { VideoPathManager } from '../video-path-manager' |
15 | import { buildFFmpegVOD } from './shared' | 16 | import { buildFFmpegVOD } from './shared' |
@@ -62,10 +63,10 @@ export async function optimizeOriginalVideofile (options: { | |||
62 | // Important to do this before getVideoFilename() to take in account the new filename | 63 | // Important to do this before getVideoFilename() to take in account the new filename |
63 | inputVideoFile.resolution = resolution | 64 | inputVideoFile.resolution = resolution |
64 | inputVideoFile.extname = newExtname | 65 | inputVideoFile.extname = newExtname |
65 | inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname) | 66 | inputVideoFile.filename = generateWebVideoFilename(resolution, newExtname) |
66 | inputVideoFile.storage = VideoStorage.FILE_SYSTEM | 67 | inputVideoFile.storage = VideoStorage.FILE_SYSTEM |
67 | 68 | ||
68 | const { videoFile } = await onWebTorrentVideoFileTranscoding({ | 69 | const { videoFile } = await onWebVideoFileTranscoding({ |
69 | video, | 70 | video, |
70 | videoFile: inputVideoFile, | 71 | videoFile: inputVideoFile, |
71 | videoOutputPath | 72 | videoOutputPath |
@@ -82,8 +83,8 @@ export async function optimizeOriginalVideofile (options: { | |||
82 | } | 83 | } |
83 | } | 84 | } |
84 | 85 | ||
85 | // Transcode the original video file to a lower resolution compatible with WebTorrent | 86 | // Transcode the original video file to a lower resolution compatible with web browsers |
86 | export async function transcodeNewWebTorrentResolution (options: { | 87 | export async function transcodeNewWebVideoResolution (options: { |
87 | video: MVideoFullLight | 88 | video: MVideoFullLight |
88 | resolution: VideoResolution | 89 | resolution: VideoResolution |
89 | fps: number | 90 | fps: number |
@@ -104,7 +105,7 @@ export async function transcodeNewWebTorrentResolution (options: { | |||
104 | const newVideoFile = new VideoFileModel({ | 105 | const newVideoFile = new VideoFileModel({ |
105 | resolution, | 106 | resolution, |
106 | extname: newExtname, | 107 | extname: newExtname, |
107 | filename: generateWebTorrentVideoFilename(resolution, newExtname), | 108 | filename: generateWebVideoFilename(resolution, newExtname), |
108 | size: 0, | 109 | size: 0, |
109 | videoId: video.id | 110 | videoId: video.id |
110 | }) | 111 | }) |
@@ -125,7 +126,7 @@ export async function transcodeNewWebTorrentResolution (options: { | |||
125 | 126 | ||
126 | await buildFFmpegVOD(job).transcode(transcodeOptions) | 127 | await buildFFmpegVOD(job).transcode(transcodeOptions) |
127 | 128 | ||
128 | return onWebTorrentVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath }) | 129 | return onWebVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath }) |
129 | }) | 130 | }) |
130 | 131 | ||
131 | return result | 132 | return result |
@@ -188,17 +189,18 @@ export async function mergeAudioVideofile (options: { | |||
188 | // Important to do this before getVideoFilename() to take in account the new file extension | 189 | // Important to do this before getVideoFilename() to take in account the new file extension |
189 | inputVideoFile.extname = newExtname | 190 | inputVideoFile.extname = newExtname |
190 | inputVideoFile.resolution = resolution | 191 | inputVideoFile.resolution = resolution |
191 | inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname) | 192 | inputVideoFile.filename = generateWebVideoFilename(inputVideoFile.resolution, newExtname) |
192 | 193 | ||
193 | // ffmpeg generated a new video file, so update the video duration | 194 | // ffmpeg generated a new video file, so update the video duration |
194 | // See https://trac.ffmpeg.org/ticket/5456 | 195 | // See https://trac.ffmpeg.org/ticket/5456 |
195 | video.duration = await getVideoStreamDuration(videoOutputPath) | 196 | video.duration = await getVideoStreamDuration(videoOutputPath) |
196 | await video.save() | 197 | await video.save() |
197 | 198 | ||
198 | return onWebTorrentVideoFileTranscoding({ | 199 | return onWebVideoFileTranscoding({ |
199 | video, | 200 | video, |
200 | videoFile: inputVideoFile, | 201 | videoFile: inputVideoFile, |
201 | videoOutputPath | 202 | videoOutputPath, |
203 | wasAudioFile: true | ||
202 | }) | 204 | }) |
203 | }) | 205 | }) |
204 | 206 | ||
@@ -208,12 +210,13 @@ export async function mergeAudioVideofile (options: { | |||
208 | } | 210 | } |
209 | } | 211 | } |
210 | 212 | ||
211 | export async function onWebTorrentVideoFileTranscoding (options: { | 213 | export async function onWebVideoFileTranscoding (options: { |
212 | video: MVideoFullLight | 214 | video: MVideoFullLight |
213 | videoFile: MVideoFile | 215 | videoFile: MVideoFile |
214 | videoOutputPath: string | 216 | videoOutputPath: string |
217 | wasAudioFile?: boolean // default false | ||
215 | }) { | 218 | }) { |
216 | const { video, videoFile, videoOutputPath } = options | 219 | const { video, videoFile, videoOutputPath, wasAudioFile } = options |
217 | 220 | ||
218 | const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) | 221 | const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
219 | 222 | ||
@@ -236,12 +239,23 @@ export async function onWebTorrentVideoFileTranscoding (options: { | |||
236 | 239 | ||
237 | await createTorrentAndSetInfoHash(video, videoFile) | 240 | await createTorrentAndSetInfoHash(video, videoFile) |
238 | 241 | ||
239 | const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution }) | 242 | const oldFile = await VideoFileModel.loadWebVideoFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution }) |
240 | if (oldFile) await video.removeWebTorrentFile(oldFile) | 243 | if (oldFile) await video.removeWebVideoFile(oldFile) |
241 | 244 | ||
242 | await VideoFileModel.customUpsert(videoFile, 'video', undefined) | 245 | await VideoFileModel.customUpsert(videoFile, 'video', undefined) |
243 | video.VideoFiles = await video.$get('VideoFiles') | 246 | video.VideoFiles = await video.$get('VideoFiles') |
244 | 247 | ||
248 | if (wasAudioFile) { | ||
249 | await JobQueue.Instance.createJob({ | ||
250 | type: 'generate-video-storyboard' as 'generate-video-storyboard', | ||
251 | payload: { | ||
252 | videoUUID: video.uuid, | ||
253 | // No need to federate, we process these jobs sequentially | ||
254 | federate: false | ||
255 | } | ||
256 | }) | ||
257 | } | ||
258 | |||
245 | return { video, videoFile } | 259 | return { video, videoFile } |
246 | } finally { | 260 | } finally { |
247 | mutexReleaser() | 261 | mutexReleaser() |
diff --git a/server/lib/video-file.ts b/server/lib/video-file.ts index 88d48c945..46af67ccd 100644 --- a/server/lib/video-file.ts +++ b/server/lib/video-file.ts | |||
@@ -7,7 +7,7 @@ import { getFileSize } from '@shared/extra-utils' | |||
7 | import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, isAudioFile } from '@shared/ffmpeg' | 7 | import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, isAudioFile } from '@shared/ffmpeg' |
8 | import { VideoFileMetadata, VideoResolution } from '@shared/models' | 8 | import { VideoFileMetadata, VideoResolution } from '@shared/models' |
9 | import { lTags } from './object-storage/shared' | 9 | import { lTags } from './object-storage/shared' |
10 | import { generateHLSVideoFilename, generateWebTorrentVideoFilename } from './paths' | 10 | import { generateHLSVideoFilename, generateWebVideoFilename } from './paths' |
11 | import { VideoPathManager } from './video-path-manager' | 11 | import { VideoPathManager } from './video-path-manager' |
12 | 12 | ||
13 | async function buildNewFile (options: { | 13 | async function buildNewFile (options: { |
@@ -33,7 +33,7 @@ async function buildNewFile (options: { | |||
33 | } | 33 | } |
34 | 34 | ||
35 | videoFile.filename = mode === 'web-video' | 35 | videoFile.filename = mode === 'web-video' |
36 | ? generateWebTorrentVideoFilename(videoFile.resolution, videoFile.extname) | 36 | ? generateWebVideoFilename(videoFile.resolution, videoFile.extname) |
37 | : generateHLSVideoFilename(videoFile.resolution) | 37 | : generateHLSVideoFilename(videoFile.resolution) |
38 | 38 | ||
39 | return videoFile | 39 | return videoFile |
@@ -85,12 +85,12 @@ async function removeHLSFile (video: MVideoWithAllFiles, fileToDeleteId: number) | |||
85 | 85 | ||
86 | // --------------------------------------------------------------------------- | 86 | // --------------------------------------------------------------------------- |
87 | 87 | ||
88 | async function removeAllWebTorrentFiles (video: MVideoWithAllFiles) { | 88 | async function removeAllWebVideoFiles (video: MVideoWithAllFiles) { |
89 | const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) | 89 | const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
90 | 90 | ||
91 | try { | 91 | try { |
92 | for (const file of video.VideoFiles) { | 92 | for (const file of video.VideoFiles) { |
93 | await video.removeWebTorrentFile(file) | 93 | await video.removeWebVideoFile(file) |
94 | await file.destroy() | 94 | await file.destroy() |
95 | } | 95 | } |
96 | 96 | ||
@@ -102,17 +102,17 @@ async function removeAllWebTorrentFiles (video: MVideoWithAllFiles) { | |||
102 | return video | 102 | return video |
103 | } | 103 | } |
104 | 104 | ||
105 | async function removeWebTorrentFile (video: MVideoWithAllFiles, fileToDeleteId: number) { | 105 | async function removeWebVideoFile (video: MVideoWithAllFiles, fileToDeleteId: number) { |
106 | const files = video.VideoFiles | 106 | const files = video.VideoFiles |
107 | 107 | ||
108 | if (files.length === 1) { | 108 | if (files.length === 1) { |
109 | return removeAllWebTorrentFiles(video) | 109 | return removeAllWebVideoFiles(video) |
110 | } | 110 | } |
111 | 111 | ||
112 | const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) | 112 | const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
113 | try { | 113 | try { |
114 | const toDelete = files.find(f => f.id === fileToDeleteId) | 114 | const toDelete = files.find(f => f.id === fileToDeleteId) |
115 | await video.removeWebTorrentFile(toDelete) | 115 | await video.removeWebVideoFile(toDelete) |
116 | await toDelete.destroy() | 116 | await toDelete.destroy() |
117 | 117 | ||
118 | video.VideoFiles = files.filter(f => f.id !== toDelete.id) | 118 | video.VideoFiles = files.filter(f => f.id !== toDelete.id) |
@@ -138,8 +138,8 @@ export { | |||
138 | 138 | ||
139 | removeHLSPlaylist, | 139 | removeHLSPlaylist, |
140 | removeHLSFile, | 140 | removeHLSFile, |
141 | removeAllWebTorrentFiles, | 141 | removeAllWebVideoFiles, |
142 | removeWebTorrentFile, | 142 | removeWebVideoFile, |
143 | 143 | ||
144 | buildFileMetadata | 144 | buildFileMetadata |
145 | } | 145 | } |
diff --git a/server/lib/video-path-manager.ts b/server/lib/video-path-manager.ts index 9953cae5d..133544bb2 100644 --- a/server/lib/video-path-manager.ts +++ b/server/lib/video-path-manager.ts | |||
@@ -8,7 +8,7 @@ import { DIRECTORIES } from '@server/initializers/constants' | |||
8 | import { MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '@server/types/models' | 8 | import { MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '@server/types/models' |
9 | import { buildUUID } from '@shared/extra-utils' | 9 | import { buildUUID } from '@shared/extra-utils' |
10 | import { VideoStorage } from '@shared/models' | 10 | import { VideoStorage } from '@shared/models' |
11 | import { makeHLSFileAvailable, makeWebTorrentFileAvailable } from './object-storage' | 11 | import { makeHLSFileAvailable, makeWebVideoFileAvailable } from './object-storage' |
12 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from './paths' | 12 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from './paths' |
13 | import { isVideoInPrivateDirectory } from './video-privacy' | 13 | import { isVideoInPrivateDirectory } from './video-privacy' |
14 | 14 | ||
@@ -78,7 +78,7 @@ class VideoPathManager { | |||
78 | } | 78 | } |
79 | 79 | ||
80 | return this.makeAvailableFactory( | 80 | return this.makeAvailableFactory( |
81 | () => makeWebTorrentFileAvailable(videoFile.filename, destination), | 81 | () => makeWebVideoFileAvailable(videoFile.filename, destination), |
82 | true, | 82 | true, |
83 | cb | 83 | cb |
84 | ) | 84 | ) |
diff --git a/server/lib/video-pre-import.ts b/server/lib/video-pre-import.ts index df67dc953..381f1f535 100644 --- a/server/lib/video-pre-import.ts +++ b/server/lib/video-pre-import.ts | |||
@@ -29,7 +29,8 @@ import { | |||
29 | } from '@server/types/models' | 29 | } from '@server/types/models' |
30 | import { ThumbnailType, VideoImportCreate, VideoImportPayload, VideoImportState, VideoPrivacy, VideoState } from '@shared/models' | 30 | import { ThumbnailType, VideoImportCreate, VideoImportPayload, VideoImportState, VideoPrivacy, VideoState } from '@shared/models' |
31 | import { getLocalVideoActivityPubUrl } from './activitypub/url' | 31 | import { getLocalVideoActivityPubUrl } from './activitypub/url' |
32 | import { updateVideoMiniatureFromExisting, updateVideoMiniatureFromUrl } from './thumbnail' | 32 | import { updateLocalVideoMiniatureFromExisting, updateLocalVideoMiniatureFromUrl } from './thumbnail' |
33 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
33 | 34 | ||
34 | class YoutubeDlImportError extends Error { | 35 | class YoutubeDlImportError extends Error { |
35 | code: YoutubeDlImportError.CODE | 36 | code: YoutubeDlImportError.CODE |
@@ -64,8 +65,9 @@ async function insertFromImportIntoDB (parameters: { | |||
64 | tags: string[] | 65 | tags: string[] |
65 | videoImportAttributes: FilteredModelAttributes<VideoImportModel> | 66 | videoImportAttributes: FilteredModelAttributes<VideoImportModel> |
66 | user: MUser | 67 | user: MUser |
68 | videoPasswords?: string[] | ||
67 | }): Promise<MVideoImportFormattable> { | 69 | }): Promise<MVideoImportFormattable> { |
68 | const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters | 70 | const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user, videoPasswords } = parameters |
69 | 71 | ||
70 | const videoImport = await sequelizeTypescript.transaction(async t => { | 72 | const videoImport = await sequelizeTypescript.transaction(async t => { |
71 | const sequelizeOptions = { transaction: t } | 73 | const sequelizeOptions = { transaction: t } |
@@ -77,6 +79,10 @@ async function insertFromImportIntoDB (parameters: { | |||
77 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | 79 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) |
78 | if (previewModel) await videoCreated.addAndSaveThumbnail(previewModel, t) | 80 | if (previewModel) await videoCreated.addAndSaveThumbnail(previewModel, t) |
79 | 81 | ||
82 | if (videoCreated.privacy === VideoPrivacy.PASSWORD_PROTECTED) { | ||
83 | await VideoPasswordModel.addPasswords(videoPasswords, video.id, t) | ||
84 | } | ||
85 | |||
80 | await autoBlacklistVideoIfNeeded({ | 86 | await autoBlacklistVideoIfNeeded({ |
81 | video: videoCreated, | 87 | video: videoCreated, |
82 | user, | 88 | user, |
@@ -208,7 +214,8 @@ async function buildYoutubeDLImport (options: { | |||
208 | state: VideoImportState.PENDING, | 214 | state: VideoImportState.PENDING, |
209 | userId: user.id, | 215 | userId: user.id, |
210 | videoChannelSyncId: channelSync?.id | 216 | videoChannelSyncId: channelSync?.id |
211 | } | 217 | }, |
218 | videoPasswords: importDataOverride.videoPasswords | ||
212 | }) | 219 | }) |
213 | 220 | ||
214 | // Get video subtitles | 221 | // Get video subtitles |
@@ -249,19 +256,22 @@ async function forgeThumbnail ({ inputPath, video, downloadUrl, type }: { | |||
249 | type: ThumbnailType | 256 | type: ThumbnailType |
250 | }): Promise<MThumbnail> { | 257 | }): Promise<MThumbnail> { |
251 | if (inputPath) { | 258 | if (inputPath) { |
252 | return updateVideoMiniatureFromExisting({ | 259 | return updateLocalVideoMiniatureFromExisting({ |
253 | inputPath, | 260 | inputPath, |
254 | video, | 261 | video, |
255 | type, | 262 | type, |
256 | automaticallyGenerated: false | 263 | automaticallyGenerated: false |
257 | }) | 264 | }) |
258 | } else if (downloadUrl) { | 265 | } |
266 | |||
267 | if (downloadUrl) { | ||
259 | try { | 268 | try { |
260 | return await updateVideoMiniatureFromUrl({ downloadUrl, video, type }) | 269 | return await updateLocalVideoMiniatureFromUrl({ downloadUrl, video, type }) |
261 | } catch (err) { | 270 | } catch (err) { |
262 | logger.warn('Cannot process thumbnail %s from youtube-dl.', downloadUrl, { err }) | 271 | logger.warn('Cannot process thumbnail %s from youtube-dl.', downloadUrl, { err }) |
263 | } | 272 | } |
264 | } | 273 | } |
274 | |||
265 | return null | 275 | return null |
266 | } | 276 | } |
267 | 277 | ||
diff --git a/server/lib/video-privacy.ts b/server/lib/video-privacy.ts index 41f9d62b3..5dd4d9781 100644 --- a/server/lib/video-privacy.ts +++ b/server/lib/video-privacy.ts | |||
@@ -4,7 +4,13 @@ import { logger } from '@server/helpers/logger' | |||
4 | import { DIRECTORIES } from '@server/initializers/constants' | 4 | import { DIRECTORIES } from '@server/initializers/constants' |
5 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 5 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
6 | import { VideoPrivacy, VideoStorage } from '@shared/models' | 6 | import { VideoPrivacy, VideoStorage } from '@shared/models' |
7 | import { updateHLSFilesACL, updateWebTorrentFileACL } from './object-storage' | 7 | import { updateHLSFilesACL, updateWebVideoFileACL } from './object-storage' |
8 | |||
9 | const validPrivacySet = new Set([ | ||
10 | VideoPrivacy.PRIVATE, | ||
11 | VideoPrivacy.INTERNAL, | ||
12 | VideoPrivacy.PASSWORD_PROTECTED | ||
13 | ]) | ||
8 | 14 | ||
9 | function setVideoPrivacy (video: MVideo, newPrivacy: VideoPrivacy) { | 15 | function setVideoPrivacy (video: MVideo, newPrivacy: VideoPrivacy) { |
10 | if (video.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) { | 16 | if (video.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) { |
@@ -14,8 +20,8 @@ function setVideoPrivacy (video: MVideo, newPrivacy: VideoPrivacy) { | |||
14 | video.privacy = newPrivacy | 20 | video.privacy = newPrivacy |
15 | } | 21 | } |
16 | 22 | ||
17 | function isVideoInPrivateDirectory (privacy: VideoPrivacy) { | 23 | function isVideoInPrivateDirectory (privacy) { |
18 | return privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.INTERNAL | 24 | return validPrivacySet.has(privacy) |
19 | } | 25 | } |
20 | 26 | ||
21 | function isVideoInPublicDirectory (privacy: VideoPrivacy) { | 27 | function isVideoInPublicDirectory (privacy: VideoPrivacy) { |
@@ -61,9 +67,9 @@ async function moveFiles (options: { | |||
61 | 67 | ||
62 | for (const file of video.VideoFiles) { | 68 | for (const file of video.VideoFiles) { |
63 | if (file.storage === VideoStorage.FILE_SYSTEM) { | 69 | if (file.storage === VideoStorage.FILE_SYSTEM) { |
64 | await moveWebTorrentFileOnFS(type, video, file) | 70 | await moveWebVideoFileOnFS(type, video, file) |
65 | } else { | 71 | } else { |
66 | await updateWebTorrentFileACL(video, file) | 72 | await updateWebVideoFileACL(video, file) |
67 | } | 73 | } |
68 | } | 74 | } |
69 | 75 | ||
@@ -78,22 +84,22 @@ async function moveFiles (options: { | |||
78 | } | 84 | } |
79 | } | 85 | } |
80 | 86 | ||
81 | async function moveWebTorrentFileOnFS (type: MoveType, video: MVideo, file: MVideoFile) { | 87 | async function moveWebVideoFileOnFS (type: MoveType, video: MVideo, file: MVideoFile) { |
82 | const directories = getWebTorrentDirectories(type) | 88 | const directories = getWebVideoDirectories(type) |
83 | 89 | ||
84 | const source = join(directories.old, file.filename) | 90 | const source = join(directories.old, file.filename) |
85 | const destination = join(directories.new, file.filename) | 91 | const destination = join(directories.new, file.filename) |
86 | 92 | ||
87 | try { | 93 | try { |
88 | logger.info('Moving WebTorrent files of %s after privacy change (%s -> %s).', video.uuid, source, destination) | 94 | logger.info('Moving web video files of %s after privacy change (%s -> %s).', video.uuid, source, destination) |
89 | 95 | ||
90 | await move(source, destination) | 96 | await move(source, destination) |
91 | } catch (err) { | 97 | } catch (err) { |
92 | logger.error('Cannot move webtorrent file %s to %s after privacy change', source, destination, { err }) | 98 | logger.error('Cannot move web video file %s to %s after privacy change', source, destination, { err }) |
93 | } | 99 | } |
94 | } | 100 | } |
95 | 101 | ||
96 | function getWebTorrentDirectories (moveType: MoveType) { | 102 | function getWebVideoDirectories (moveType: MoveType) { |
97 | if (moveType === 'private-to-public') { | 103 | if (moveType === 'private-to-public') { |
98 | return { old: DIRECTORIES.VIDEOS.PRIVATE, new: DIRECTORIES.VIDEOS.PUBLIC } | 104 | return { old: DIRECTORIES.VIDEOS.PRIVATE, new: DIRECTORIES.VIDEOS.PUBLIC } |
99 | } | 105 | } |
diff --git a/server/lib/video-studio.ts b/server/lib/video-studio.ts index 0d3db8f60..f549a7084 100644 --- a/server/lib/video-studio.ts +++ b/server/lib/video-studio.ts | |||
@@ -12,7 +12,7 @@ import { JobQueue } from './job-queue' | |||
12 | import { VideoStudioTranscodingJobHandler } from './runners' | 12 | import { VideoStudioTranscodingJobHandler } from './runners' |
13 | import { createOptimizeOrMergeAudioJobs } from './transcoding/create-transcoding-job' | 13 | import { createOptimizeOrMergeAudioJobs } from './transcoding/create-transcoding-job' |
14 | import { getTranscodingJobPriority } from './transcoding/transcoding-priority' | 14 | import { getTranscodingJobPriority } from './transcoding/transcoding-priority' |
15 | import { buildNewFile, removeHLSPlaylist, removeWebTorrentFile } from './video-file' | 15 | import { buildNewFile, removeHLSPlaylist, removeWebVideoFile } from './video-file' |
16 | import { VideoPathManager } from './video-path-manager' | 16 | import { VideoPathManager } from './video-path-manager' |
17 | 17 | ||
18 | const lTags = loggerTagsFactory('video-studio') | 18 | const lTags = loggerTagsFactory('video-studio') |
@@ -119,12 +119,12 @@ export async function onVideoStudioEnded (options: { | |||
119 | // Private | 119 | // Private |
120 | // --------------------------------------------------------------------------- | 120 | // --------------------------------------------------------------------------- |
121 | 121 | ||
122 | async function removeAllFiles (video: MVideoWithAllFiles, webTorrentFileException: MVideoFile) { | 122 | async function removeAllFiles (video: MVideoWithAllFiles, webVideoFileException: MVideoFile) { |
123 | await removeHLSPlaylist(video) | 123 | await removeHLSPlaylist(video) |
124 | 124 | ||
125 | for (const file of video.VideoFiles) { | 125 | for (const file of video.VideoFiles) { |
126 | if (file.id === webTorrentFileException.id) continue | 126 | if (file.id === webVideoFileException.id) continue |
127 | 127 | ||
128 | await removeWebTorrentFile(video, file.id) | 128 | await removeWebVideoFile(video, file.id) |
129 | } | 129 | } |
130 | } | 130 | } |
diff --git a/server/lib/video-tokens-manager.ts b/server/lib/video-tokens-manager.ts index 660533528..e28e55cf7 100644 --- a/server/lib/video-tokens-manager.ts +++ b/server/lib/video-tokens-manager.ts | |||
@@ -12,26 +12,34 @@ class VideoTokensManager { | |||
12 | 12 | ||
13 | private static instance: VideoTokensManager | 13 | private static instance: VideoTokensManager |
14 | 14 | ||
15 | private readonly lruCache = new LRUCache<string, { videoUUID: string, user: MUserAccountUrl }>({ | 15 | private readonly lruCache = new LRUCache<string, { videoUUID: string, user?: MUserAccountUrl }>({ |
16 | max: LRU_CACHE.VIDEO_TOKENS.MAX_SIZE, | 16 | max: LRU_CACHE.VIDEO_TOKENS.MAX_SIZE, |
17 | ttl: LRU_CACHE.VIDEO_TOKENS.TTL | 17 | ttl: LRU_CACHE.VIDEO_TOKENS.TTL |
18 | }) | 18 | }) |
19 | 19 | ||
20 | private constructor () {} | 20 | private constructor () {} |
21 | 21 | ||
22 | create (options: { | 22 | createForAuthUser (options: { |
23 | user: MUserAccountUrl | 23 | user: MUserAccountUrl |
24 | videoUUID: string | 24 | videoUUID: string |
25 | }) { | 25 | }) { |
26 | const token = buildUUID() | 26 | const { token, expires } = this.generateVideoToken() |
27 | |||
28 | const expires = new Date(new Date().getTime() + LRU_CACHE.VIDEO_TOKENS.TTL) | ||
29 | 27 | ||
30 | this.lruCache.set(token, pick(options, [ 'user', 'videoUUID' ])) | 28 | this.lruCache.set(token, pick(options, [ 'user', 'videoUUID' ])) |
31 | 29 | ||
32 | return { token, expires } | 30 | return { token, expires } |
33 | } | 31 | } |
34 | 32 | ||
33 | createForPasswordProtectedVideo (options: { | ||
34 | videoUUID: string | ||
35 | }) { | ||
36 | const { token, expires } = this.generateVideoToken() | ||
37 | |||
38 | this.lruCache.set(token, pick(options, [ 'videoUUID' ])) | ||
39 | |||
40 | return { token, expires } | ||
41 | } | ||
42 | |||
35 | hasToken (options: { | 43 | hasToken (options: { |
36 | token: string | 44 | token: string |
37 | videoUUID: string | 45 | videoUUID: string |
@@ -54,6 +62,13 @@ class VideoTokensManager { | |||
54 | static get Instance () { | 62 | static get Instance () { |
55 | return this.instance || (this.instance = new this()) | 63 | return this.instance || (this.instance = new this()) |
56 | } | 64 | } |
65 | |||
66 | private generateVideoToken () { | ||
67 | const token = buildUUID() | ||
68 | const expires = new Date(new Date().getTime() + LRU_CACHE.VIDEO_TOKENS.TTL) | ||
69 | |||
70 | return { token, expires } | ||
71 | } | ||
57 | } | 72 | } |
58 | 73 | ||
59 | // --------------------------------------------------------------------------- | 74 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/video-urls.ts b/server/lib/video-urls.ts index 64c2c9bf9..0597488ad 100644 --- a/server/lib/video-urls.ts +++ b/server/lib/video-urls.ts | |||
@@ -9,7 +9,7 @@ function generateHLSRedundancyUrl (video: MVideo, playlist: MStreamingPlaylist) | |||
9 | return WEBSERVER.URL + STATIC_PATHS.REDUNDANCY + playlist.getStringType() + '/' + video.uuid | 9 | return WEBSERVER.URL + STATIC_PATHS.REDUNDANCY + playlist.getStringType() + '/' + video.uuid |
10 | } | 10 | } |
11 | 11 | ||
12 | function generateWebTorrentRedundancyUrl (file: MVideoFile) { | 12 | function generateWebVideoRedundancyUrl (file: MVideoFile) { |
13 | return WEBSERVER.URL + STATIC_PATHS.REDUNDANCY + file.filename | 13 | return WEBSERVER.URL + STATIC_PATHS.REDUNDANCY + file.filename |
14 | } | 14 | } |
15 | 15 | ||
@@ -26,6 +26,6 @@ function getLocalVideoFileMetadataUrl (video: MVideoUUID, videoFile: MVideoFile) | |||
26 | export { | 26 | export { |
27 | getLocalVideoFileMetadataUrl, | 27 | getLocalVideoFileMetadataUrl, |
28 | 28 | ||
29 | generateWebTorrentRedundancyUrl, | 29 | generateWebVideoRedundancyUrl, |
30 | generateHLSRedundancyUrl | 30 | generateHLSRedundancyUrl |
31 | } | 31 | } |
diff --git a/server/lib/video.ts b/server/lib/video.ts index 588dc553f..362c861a5 100644 --- a/server/lib/video.ts +++ b/server/lib/video.ts | |||
@@ -10,7 +10,7 @@ import { FilteredModelAttributes } from '@server/types' | |||
10 | import { MThumbnail, MVideoFullLight, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' | 10 | import { MThumbnail, MVideoFullLight, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' |
11 | import { ManageVideoTorrentPayload, ThumbnailType, VideoCreate, VideoPrivacy, VideoState } from '@shared/models' | 11 | import { ManageVideoTorrentPayload, ThumbnailType, VideoCreate, VideoPrivacy, VideoState } from '@shared/models' |
12 | import { CreateJobArgument, JobQueue } from './job-queue/job-queue' | 12 | import { CreateJobArgument, JobQueue } from './job-queue/job-queue' |
13 | import { updateVideoMiniatureFromExisting } from './thumbnail' | 13 | import { updateLocalVideoMiniatureFromExisting } from './thumbnail' |
14 | import { moveFilesIfPrivacyChanged } from './video-privacy' | 14 | import { moveFilesIfPrivacyChanged } from './video-privacy' |
15 | 15 | ||
16 | function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> { | 16 | function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> { |
@@ -55,7 +55,7 @@ async function buildVideoThumbnailsFromReq (options: { | |||
55 | const fields = files?.[p.fieldName] | 55 | const fields = files?.[p.fieldName] |
56 | 56 | ||
57 | if (fields) { | 57 | if (fields) { |
58 | return updateVideoMiniatureFromExisting({ | 58 | return updateLocalVideoMiniatureFromExisting({ |
59 | inputPath: fields[0].path, | 59 | inputPath: fields[0].path, |
60 | video, | 60 | video, |
61 | type: p.type, | 61 | type: p.type, |
diff --git a/server/lib/worker/workers/image-downloader.ts b/server/lib/worker/workers/image-downloader.ts index 4b32f723e..209594589 100644 --- a/server/lib/worker/workers/image-downloader.ts +++ b/server/lib/worker/workers/image-downloader.ts | |||
@@ -24,6 +24,8 @@ async function downloadImage (options: { | |||
24 | 24 | ||
25 | throw err | 25 | throw err |
26 | } | 26 | } |
27 | |||
28 | return destPath | ||
27 | } | 29 | } |
28 | 30 | ||
29 | module.exports = downloadImage | 31 | module.exports = downloadImage |
diff --git a/server/middlewares/auth.ts b/server/middlewares/auth.ts index 0eefa2a8e..39a7b2998 100644 --- a/server/middlewares/auth.ts +++ b/server/middlewares/auth.ts | |||
@@ -5,6 +5,7 @@ import { RunnerModel } from '@server/models/runner/runner' | |||
5 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' | 5 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' |
6 | import { logger } from '../helpers/logger' | 6 | import { logger } from '../helpers/logger' |
7 | import { handleOAuthAuthenticate } from '../lib/auth/oauth' | 7 | import { handleOAuthAuthenticate } from '../lib/auth/oauth' |
8 | import { ServerErrorCode } from '@shared/models' | ||
8 | 9 | ||
9 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) { | 10 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) { |
10 | handleOAuthAuthenticate(req, res) | 11 | handleOAuthAuthenticate(req, res) |
@@ -48,15 +49,23 @@ function authenticateSocket (socket: Socket, next: (err?: any) => void) { | |||
48 | .catch(err => logger.error('Cannot get access token.', { err })) | 49 | .catch(err => logger.error('Cannot get access token.', { err })) |
49 | } | 50 | } |
50 | 51 | ||
51 | function authenticatePromise (req: express.Request, res: express.Response) { | 52 | function authenticatePromise (options: { |
53 | req: express.Request | ||
54 | res: express.Response | ||
55 | errorMessage?: string | ||
56 | errorStatus?: HttpStatusCode | ||
57 | errorType?: ServerErrorCode | ||
58 | }) { | ||
59 | const { req, res, errorMessage = 'Not authenticated', errorStatus = HttpStatusCode.UNAUTHORIZED_401, errorType } = options | ||
52 | return new Promise<void>(resolve => { | 60 | return new Promise<void>(resolve => { |
53 | // Already authenticated? (or tried to) | 61 | // Already authenticated? (or tried to) |
54 | if (res.locals.oauth?.token.User) return resolve() | 62 | if (res.locals.oauth?.token.User) return resolve() |
55 | 63 | ||
56 | if (res.locals.authenticated === false) { | 64 | if (res.locals.authenticated === false) { |
57 | return res.fail({ | 65 | return res.fail({ |
58 | status: HttpStatusCode.UNAUTHORIZED_401, | 66 | status: errorStatus, |
59 | message: 'Not authenticated' | 67 | type: errorType, |
68 | message: errorMessage | ||
60 | }) | 69 | }) |
61 | } | 70 | } |
62 | 71 | ||
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index a0074cb24..a6dbba524 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts | |||
@@ -25,6 +25,7 @@ const customConfigUpdateValidator = [ | |||
25 | body('cache.previews.size').isInt(), | 25 | body('cache.previews.size').isInt(), |
26 | body('cache.captions.size').isInt(), | 26 | body('cache.captions.size').isInt(), |
27 | body('cache.torrents.size').isInt(), | 27 | body('cache.torrents.size').isInt(), |
28 | body('cache.storyboards.size').isInt(), | ||
28 | 29 | ||
29 | body('signup.enabled').isBoolean(), | 30 | body('signup.enabled').isBoolean(), |
30 | body('signup.limit').isInt(), | 31 | body('signup.limit').isInt(), |
@@ -58,7 +59,7 @@ const customConfigUpdateValidator = [ | |||
58 | 59 | ||
59 | body('transcoding.alwaysTranscodeOriginalResolution').isBoolean(), | 60 | body('transcoding.alwaysTranscodeOriginalResolution').isBoolean(), |
60 | 61 | ||
61 | body('transcoding.webtorrent.enabled').isBoolean(), | 62 | body('transcoding.webVideos.enabled').isBoolean(), |
62 | body('transcoding.hls.enabled').isBoolean(), | 63 | body('transcoding.hls.enabled').isBoolean(), |
63 | 64 | ||
64 | body('videoStudio.enabled').isBoolean(), | 65 | body('videoStudio.enabled').isBoolean(), |
@@ -152,8 +153,8 @@ function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: exp | |||
152 | function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express.Response) { | 153 | function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express.Response) { |
153 | if (customConfig.transcoding.enabled === false) return true | 154 | if (customConfig.transcoding.enabled === false) return true |
154 | 155 | ||
155 | if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) { | 156 | if (customConfig.transcoding.webVideos.enabled === false && customConfig.transcoding.hls.enabled === false) { |
156 | res.fail({ message: 'You need to enable at least webtorrent transcoding or hls transcoding' }) | 157 | res.fail({ message: 'You need to enable at least web_videos transcoding or hls transcoding' }) |
157 | return false | 158 | return false |
158 | } | 159 | } |
159 | 160 | ||
diff --git a/server/middlewares/validators/shared/index.ts b/server/middlewares/validators/shared/index.ts index de98cd442..e5cff2dda 100644 --- a/server/middlewares/validators/shared/index.ts +++ b/server/middlewares/validators/shared/index.ts | |||
@@ -10,4 +10,5 @@ export * from './video-comments' | |||
10 | export * from './video-imports' | 10 | export * from './video-imports' |
11 | export * from './video-ownerships' | 11 | export * from './video-ownerships' |
12 | export * from './video-playlists' | 12 | export * from './video-playlists' |
13 | export * from './video-passwords' | ||
13 | export * from './videos' | 14 | export * from './videos' |
diff --git a/server/middlewares/validators/shared/video-passwords.ts b/server/middlewares/validators/shared/video-passwords.ts new file mode 100644 index 000000000..efcc95dc4 --- /dev/null +++ b/server/middlewares/validators/shared/video-passwords.ts | |||
@@ -0,0 +1,80 @@ | |||
1 | import express from 'express' | ||
2 | import { HttpStatusCode, UserRight, VideoPrivacy } from '@shared/models' | ||
3 | import { forceNumber } from '@shared/core-utils' | ||
4 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
5 | import { header } from 'express-validator' | ||
6 | import { getVideoWithAttributes } from '@server/helpers/video' | ||
7 | |||
8 | function isValidVideoPasswordHeader () { | ||
9 | return header('x-peertube-video-password') | ||
10 | .optional() | ||
11 | .isString() | ||
12 | } | ||
13 | |||
14 | function checkVideoIsPasswordProtected (res: express.Response) { | ||
15 | const video = getVideoWithAttributes(res) | ||
16 | if (video.privacy !== VideoPrivacy.PASSWORD_PROTECTED) { | ||
17 | res.fail({ | ||
18 | status: HttpStatusCode.BAD_REQUEST_400, | ||
19 | message: 'Video is not password protected' | ||
20 | }) | ||
21 | return false | ||
22 | } | ||
23 | |||
24 | return true | ||
25 | } | ||
26 | |||
27 | async function doesVideoPasswordExist (idArg: number | string, res: express.Response) { | ||
28 | const video = getVideoWithAttributes(res) | ||
29 | const id = forceNumber(idArg) | ||
30 | const videoPassword = await VideoPasswordModel.loadByIdAndVideo({ id, videoId: video.id }) | ||
31 | |||
32 | if (!videoPassword) { | ||
33 | res.fail({ | ||
34 | status: HttpStatusCode.NOT_FOUND_404, | ||
35 | message: 'Video password not found' | ||
36 | }) | ||
37 | return false | ||
38 | } | ||
39 | |||
40 | res.locals.videoPassword = videoPassword | ||
41 | |||
42 | return true | ||
43 | } | ||
44 | |||
45 | async function isVideoPasswordDeletable (res: express.Response) { | ||
46 | const user = res.locals.oauth.token.User | ||
47 | const userAccount = user.Account | ||
48 | const video = res.locals.videoAll | ||
49 | |||
50 | // Check if the user who did the request is able to delete the video passwords | ||
51 | if ( | ||
52 | user.hasRight(UserRight.UPDATE_ANY_VIDEO) === false && // Not a moderator | ||
53 | video.VideoChannel.accountId !== userAccount.id // Not the video owner | ||
54 | ) { | ||
55 | res.fail({ | ||
56 | status: HttpStatusCode.FORBIDDEN_403, | ||
57 | message: 'Cannot remove passwords of another user\'s video' | ||
58 | }) | ||
59 | return false | ||
60 | } | ||
61 | |||
62 | const passwordCount = await VideoPasswordModel.countByVideoId(video.id) | ||
63 | |||
64 | if (passwordCount <= 1) { | ||
65 | res.fail({ | ||
66 | status: HttpStatusCode.BAD_REQUEST_400, | ||
67 | message: 'Cannot delete the last password of the protected video' | ||
68 | }) | ||
69 | return false | ||
70 | } | ||
71 | |||
72 | return true | ||
73 | } | ||
74 | |||
75 | export { | ||
76 | isValidVideoPasswordHeader, | ||
77 | checkVideoIsPasswordProtected as isVideoPasswordProtected, | ||
78 | doesVideoPasswordExist, | ||
79 | isVideoPasswordDeletable | ||
80 | } | ||
diff --git a/server/middlewares/validators/shared/videos.ts b/server/middlewares/validators/shared/videos.ts index 0033a32ff..9a7497007 100644 --- a/server/middlewares/validators/shared/videos.ts +++ b/server/middlewares/validators/shared/videos.ts | |||
@@ -20,6 +20,8 @@ import { | |||
20 | MVideoWithRights | 20 | MVideoWithRights |
21 | } from '@server/types/models' | 21 | } from '@server/types/models' |
22 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoPrivacy } from '@shared/models' | 22 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoPrivacy } from '@shared/models' |
23 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
24 | import { exists } from '@server/helpers/custom-validators/misc' | ||
23 | 25 | ||
24 | async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') { | 26 | async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') { |
25 | const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined | 27 | const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined |
@@ -111,8 +113,12 @@ async function checkCanSeeVideo (options: { | |||
111 | }) { | 113 | }) { |
112 | const { req, res, video, paramId } = options | 114 | const { req, res, video, paramId } = options |
113 | 115 | ||
114 | if (video.requiresAuth({ urlParamId: paramId, checkBlacklist: true })) { | 116 | if (video.requiresUserAuth({ urlParamId: paramId, checkBlacklist: true })) { |
115 | return checkCanSeeAuthVideo(req, res, video) | 117 | return checkCanSeeUserAuthVideo({ req, res, video }) |
118 | } | ||
119 | |||
120 | if (video.privacy === VideoPrivacy.PASSWORD_PROTECTED) { | ||
121 | return checkCanSeePasswordProtectedVideo({ req, res, video }) | ||
116 | } | 122 | } |
117 | 123 | ||
118 | if (video.privacy === VideoPrivacy.UNLISTED || video.privacy === VideoPrivacy.PUBLIC) { | 124 | if (video.privacy === VideoPrivacy.UNLISTED || video.privacy === VideoPrivacy.PUBLIC) { |
@@ -122,7 +128,13 @@ async function checkCanSeeVideo (options: { | |||
122 | throw new Error('Unknown video privacy when checking video right ' + video.url) | 128 | throw new Error('Unknown video privacy when checking video right ' + video.url) |
123 | } | 129 | } |
124 | 130 | ||
125 | async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoId | MVideoWithRights) { | 131 | async function checkCanSeeUserAuthVideo (options: { |
132 | req: Request | ||
133 | res: Response | ||
134 | video: MVideoId | MVideoWithRights | ||
135 | }) { | ||
136 | const { req, res, video } = options | ||
137 | |||
126 | const fail = () => { | 138 | const fail = () => { |
127 | res.fail({ | 139 | res.fail({ |
128 | status: HttpStatusCode.FORBIDDEN_403, | 140 | status: HttpStatusCode.FORBIDDEN_403, |
@@ -132,14 +144,12 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI | |||
132 | return false | 144 | return false |
133 | } | 145 | } |
134 | 146 | ||
135 | await authenticatePromise(req, res) | 147 | await authenticatePromise({ req, res }) |
136 | 148 | ||
137 | const user = res.locals.oauth?.token.User | 149 | const user = res.locals.oauth?.token.User |
138 | if (!user) return fail() | 150 | if (!user) return fail() |
139 | 151 | ||
140 | const videoWithRights = (video as MVideoWithRights).VideoChannel?.Account?.userId | 152 | const videoWithRights = await getVideoWithRights(video as MVideoWithRights) |
141 | ? video as MVideoWithRights | ||
142 | : await VideoModel.loadFull(video.id) | ||
143 | 153 | ||
144 | const privacy = videoWithRights.privacy | 154 | const privacy = videoWithRights.privacy |
145 | 155 | ||
@@ -148,16 +158,14 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI | |||
148 | return true | 158 | return true |
149 | } | 159 | } |
150 | 160 | ||
151 | const isOwnedByUser = videoWithRights.VideoChannel.Account.userId === user.id | ||
152 | |||
153 | if (videoWithRights.isBlacklisted()) { | 161 | if (videoWithRights.isBlacklisted()) { |
154 | if (isOwnedByUser || user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) return true | 162 | if (canUserAccessVideo(user, videoWithRights, UserRight.MANAGE_VIDEO_BLACKLIST)) return true |
155 | 163 | ||
156 | return fail() | 164 | return fail() |
157 | } | 165 | } |
158 | 166 | ||
159 | if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) { | 167 | if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) { |
160 | if (isOwnedByUser || user.hasRight(UserRight.SEE_ALL_VIDEOS)) return true | 168 | if (canUserAccessVideo(user, videoWithRights, UserRight.SEE_ALL_VIDEOS)) return true |
161 | 169 | ||
162 | return fail() | 170 | return fail() |
163 | } | 171 | } |
@@ -166,6 +174,59 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI | |||
166 | return fail() | 174 | return fail() |
167 | } | 175 | } |
168 | 176 | ||
177 | async function checkCanSeePasswordProtectedVideo (options: { | ||
178 | req: Request | ||
179 | res: Response | ||
180 | video: MVideo | ||
181 | }) { | ||
182 | const { req, res, video } = options | ||
183 | |||
184 | const videoWithRights = await getVideoWithRights(video as MVideoWithRights) | ||
185 | |||
186 | const videoPassword = req.header('x-peertube-video-password') | ||
187 | |||
188 | if (!exists(videoPassword)) { | ||
189 | const errorMessage = 'Please provide a password to access this password protected video' | ||
190 | const errorType = ServerErrorCode.VIDEO_REQUIRES_PASSWORD | ||
191 | |||
192 | if (req.header('authorization')) { | ||
193 | await authenticatePromise({ req, res, errorMessage, errorStatus: HttpStatusCode.FORBIDDEN_403, errorType }) | ||
194 | const user = res.locals.oauth?.token.User | ||
195 | |||
196 | if (canUserAccessVideo(user, videoWithRights, UserRight.SEE_ALL_VIDEOS)) return true | ||
197 | } | ||
198 | |||
199 | res.fail({ | ||
200 | status: HttpStatusCode.FORBIDDEN_403, | ||
201 | type: errorType, | ||
202 | message: errorMessage | ||
203 | }) | ||
204 | return false | ||
205 | } | ||
206 | |||
207 | if (await VideoPasswordModel.isACorrectPassword({ videoId: video.id, password: videoPassword })) return true | ||
208 | |||
209 | res.fail({ | ||
210 | status: HttpStatusCode.FORBIDDEN_403, | ||
211 | type: ServerErrorCode.INCORRECT_VIDEO_PASSWORD, | ||
212 | message: 'Incorrect video password. Access to the video is denied.' | ||
213 | }) | ||
214 | |||
215 | return false | ||
216 | } | ||
217 | |||
218 | function canUserAccessVideo (user: MUser, video: MVideoWithRights | MVideoAccountLight, right: UserRight) { | ||
219 | const isOwnedByUser = video.VideoChannel.Account.userId === user.id | ||
220 | |||
221 | return isOwnedByUser || user.hasRight(right) | ||
222 | } | ||
223 | |||
224 | async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithRights> { | ||
225 | return video.VideoChannel?.Account?.userId | ||
226 | ? video | ||
227 | : VideoModel.loadFull(video.id) | ||
228 | } | ||
229 | |||
169 | // --------------------------------------------------------------------------- | 230 | // --------------------------------------------------------------------------- |
170 | 231 | ||
171 | async function checkCanAccessVideoStaticFiles (options: { | 232 | async function checkCanAccessVideoStaticFiles (options: { |
@@ -176,7 +237,7 @@ async function checkCanAccessVideoStaticFiles (options: { | |||
176 | }) { | 237 | }) { |
177 | const { video, req, res } = options | 238 | const { video, req, res } = options |
178 | 239 | ||
179 | if (res.locals.oauth?.token.User) { | 240 | if (res.locals.oauth?.token.User || exists(req.header('x-peertube-video-password'))) { |
180 | return checkCanSeeVideo(options) | 241 | return checkCanSeeVideo(options) |
181 | } | 242 | } |
182 | 243 | ||
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index 959f663ac..07d6cba82 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -28,6 +28,7 @@ export const pluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.PLUGINS) | |||
28 | export const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) | 28 | export const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) |
29 | export const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES) | 29 | export const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES) |
30 | export const videoChannelSyncsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_SYNCS) | 30 | export const videoChannelSyncsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_SYNCS) |
31 | export const videoPasswordsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PASSWORDS) | ||
31 | 32 | ||
32 | export const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS) | 33 | export const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS) |
33 | export const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS) | 34 | export const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS) |
diff --git a/server/middlewares/validators/static.ts b/server/middlewares/validators/static.ts index 9c2d890ba..86cc0a8d7 100644 --- a/server/middlewares/validators/static.ts +++ b/server/middlewares/validators/static.ts | |||
@@ -9,7 +9,7 @@ import { VideoModel } from '@server/models/video/video' | |||
9 | import { VideoFileModel } from '@server/models/video/video-file' | 9 | import { VideoFileModel } from '@server/models/video/video-file' |
10 | import { MStreamingPlaylist, MVideoFile, MVideoThumbnail } from '@server/types/models' | 10 | import { MStreamingPlaylist, MVideoFile, MVideoThumbnail } from '@server/types/models' |
11 | import { HttpStatusCode } from '@shared/models' | 11 | import { HttpStatusCode } from '@shared/models' |
12 | import { areValidationErrors, checkCanAccessVideoStaticFiles } from './shared' | 12 | import { areValidationErrors, checkCanAccessVideoStaticFiles, isValidVideoPasswordHeader } from './shared' |
13 | 13 | ||
14 | type LRUValue = { | 14 | type LRUValue = { |
15 | allowed: boolean | 15 | allowed: boolean |
@@ -22,9 +22,11 @@ const staticFileTokenBypass = new LRUCache<string, LRUValue>({ | |||
22 | ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL | 22 | ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL |
23 | }) | 23 | }) |
24 | 24 | ||
25 | const ensureCanAccessVideoPrivateWebTorrentFiles = [ | 25 | const ensureCanAccessVideoPrivateWebVideoFiles = [ |
26 | query('videoFileToken').optional().custom(exists), | 26 | query('videoFileToken').optional().custom(exists), |
27 | 27 | ||
28 | isValidVideoPasswordHeader(), | ||
29 | |||
28 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 30 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
29 | if (areValidationErrors(req, res)) return | 31 | if (areValidationErrors(req, res)) return |
30 | 32 | ||
@@ -46,7 +48,7 @@ const ensureCanAccessVideoPrivateWebTorrentFiles = [ | |||
46 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | 48 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) |
47 | } | 49 | } |
48 | 50 | ||
49 | const result = await isWebTorrentAllowed(req, res) | 51 | const result = await isWebVideoAllowed(req, res) |
50 | 52 | ||
51 | staticFileTokenBypass.set(cacheKey, result) | 53 | staticFileTokenBypass.set(cacheKey, result) |
52 | 54 | ||
@@ -73,6 +75,8 @@ const ensureCanAccessPrivateVideoHLSFiles = [ | |||
73 | .optional() | 75 | .optional() |
74 | .customSanitizer(isSafePeerTubeFilenameWithoutExtension), | 76 | .customSanitizer(isSafePeerTubeFilenameWithoutExtension), |
75 | 77 | ||
78 | isValidVideoPasswordHeader(), | ||
79 | |||
76 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 80 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
77 | if (areValidationErrors(req, res)) return | 81 | if (areValidationErrors(req, res)) return |
78 | 82 | ||
@@ -118,13 +122,13 @@ const ensureCanAccessPrivateVideoHLSFiles = [ | |||
118 | ] | 122 | ] |
119 | 123 | ||
120 | export { | 124 | export { |
121 | ensureCanAccessVideoPrivateWebTorrentFiles, | 125 | ensureCanAccessVideoPrivateWebVideoFiles, |
122 | ensureCanAccessPrivateVideoHLSFiles | 126 | ensureCanAccessPrivateVideoHLSFiles |
123 | } | 127 | } |
124 | 128 | ||
125 | // --------------------------------------------------------------------------- | 129 | // --------------------------------------------------------------------------- |
126 | 130 | ||
127 | async function isWebTorrentAllowed (req: express.Request, res: express.Response) { | 131 | async function isWebVideoAllowed (req: express.Request, res: express.Response) { |
128 | const filename = basename(req.path) | 132 | const filename = basename(req.path) |
129 | 133 | ||
130 | const file = await VideoFileModel.loadWithVideoByFilename(filename) | 134 | const file = await VideoFileModel.loadWithVideoByFilename(filename) |
@@ -167,11 +171,11 @@ async function isHLSAllowed (req: express.Request, res: express.Response, videoU | |||
167 | } | 171 | } |
168 | 172 | ||
169 | function extractTokenOrDie (req: express.Request, res: express.Response) { | 173 | function extractTokenOrDie (req: express.Request, res: express.Response) { |
170 | const token = res.locals.oauth?.token.accessToken || req.query.videoFileToken | 174 | const token = req.header('x-peertube-video-password') || req.query.videoFileToken || res.locals.oauth?.token.accessToken |
171 | 175 | ||
172 | if (!token) { | 176 | if (!token) { |
173 | return res.fail({ | 177 | return res.fail({ |
174 | message: 'Bearer token is missing in headers or video file token is missing in URL query parameters', | 178 | message: 'Video password header, video file token query parameter and bearer token are all missing', // |
175 | status: HttpStatusCode.FORBIDDEN_403 | 179 | status: HttpStatusCode.FORBIDDEN_403 |
176 | }) | 180 | }) |
177 | } | 181 | } |
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts index d225dfe45..0c824c314 100644 --- a/server/middlewares/validators/videos/index.ts +++ b/server/middlewares/validators/videos/index.ts | |||
@@ -12,6 +12,8 @@ export * from './video-shares' | |||
12 | export * from './video-source' | 12 | export * from './video-source' |
13 | export * from './video-stats' | 13 | export * from './video-stats' |
14 | export * from './video-studio' | 14 | export * from './video-studio' |
15 | export * from './video-token' | ||
15 | export * from './video-transcoding' | 16 | export * from './video-transcoding' |
16 | export * from './videos' | 17 | export * from './videos' |
17 | export * from './video-channel-sync' | 18 | export * from './video-channel-sync' |
19 | export * from './video-passwords' | ||
diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts index 72b2febc3..077a58d2e 100644 --- a/server/middlewares/validators/videos/video-captions.ts +++ b/server/middlewares/validators/videos/video-captions.ts | |||
@@ -10,7 +10,8 @@ import { | |||
10 | checkUserCanManageVideo, | 10 | checkUserCanManageVideo, |
11 | doesVideoCaptionExist, | 11 | doesVideoCaptionExist, |
12 | doesVideoExist, | 12 | doesVideoExist, |
13 | isValidVideoIdParam | 13 | isValidVideoIdParam, |
14 | isValidVideoPasswordHeader | ||
14 | } from '../shared' | 15 | } from '../shared' |
15 | 16 | ||
16 | const addVideoCaptionValidator = [ | 17 | const addVideoCaptionValidator = [ |
@@ -62,6 +63,8 @@ const deleteVideoCaptionValidator = [ | |||
62 | const listVideoCaptionsValidator = [ | 63 | const listVideoCaptionsValidator = [ |
63 | isValidVideoIdParam('videoId'), | 64 | isValidVideoIdParam('videoId'), |
64 | 65 | ||
66 | isValidVideoPasswordHeader(), | ||
67 | |||
65 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 68 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
66 | if (areValidationErrors(req, res)) return | 69 | if (areValidationErrors(req, res)) return |
67 | if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return | 70 | if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return |
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts index 133feb7bd..70689b02e 100644 --- a/server/middlewares/validators/videos/video-comments.ts +++ b/server/middlewares/validators/videos/video-comments.ts | |||
@@ -14,7 +14,8 @@ import { | |||
14 | doesVideoCommentExist, | 14 | doesVideoCommentExist, |
15 | doesVideoCommentThreadExist, | 15 | doesVideoCommentThreadExist, |
16 | doesVideoExist, | 16 | doesVideoExist, |
17 | isValidVideoIdParam | 17 | isValidVideoIdParam, |
18 | isValidVideoPasswordHeader | ||
18 | } from '../shared' | 19 | } from '../shared' |
19 | 20 | ||
20 | const listVideoCommentsValidator = [ | 21 | const listVideoCommentsValidator = [ |
@@ -51,6 +52,7 @@ const listVideoCommentsValidator = [ | |||
51 | 52 | ||
52 | const listVideoCommentThreadsValidator = [ | 53 | const listVideoCommentThreadsValidator = [ |
53 | isValidVideoIdParam('videoId'), | 54 | isValidVideoIdParam('videoId'), |
55 | isValidVideoPasswordHeader(), | ||
54 | 56 | ||
55 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 57 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
56 | if (areValidationErrors(req, res)) return | 58 | if (areValidationErrors(req, res)) return |
@@ -67,6 +69,7 @@ const listVideoThreadCommentsValidator = [ | |||
67 | 69 | ||
68 | param('threadId') | 70 | param('threadId') |
69 | .custom(isIdValid), | 71 | .custom(isIdValid), |
72 | isValidVideoPasswordHeader(), | ||
70 | 73 | ||
71 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 74 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
72 | if (areValidationErrors(req, res)) return | 75 | if (areValidationErrors(req, res)) return |
@@ -84,6 +87,7 @@ const addVideoCommentThreadValidator = [ | |||
84 | 87 | ||
85 | body('text') | 88 | body('text') |
86 | .custom(isValidVideoCommentText), | 89 | .custom(isValidVideoCommentText), |
90 | isValidVideoPasswordHeader(), | ||
87 | 91 | ||
88 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 92 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
89 | if (areValidationErrors(req, res)) return | 93 | if (areValidationErrors(req, res)) return |
@@ -102,6 +106,7 @@ const addVideoCommentReplyValidator = [ | |||
102 | isValidVideoIdParam('videoId'), | 106 | isValidVideoIdParam('videoId'), |
103 | 107 | ||
104 | param('commentId').custom(isIdValid), | 108 | param('commentId').custom(isIdValid), |
109 | isValidVideoPasswordHeader(), | ||
105 | 110 | ||
106 | body('text').custom(isValidVideoCommentText), | 111 | body('text').custom(isValidVideoCommentText), |
107 | 112 | ||
diff --git a/server/middlewares/validators/videos/video-files.ts b/server/middlewares/validators/videos/video-files.ts index 92c5b9483..6c0ecda42 100644 --- a/server/middlewares/validators/videos/video-files.ts +++ b/server/middlewares/validators/videos/video-files.ts | |||
@@ -5,7 +5,7 @@ import { MVideo } from '@server/types/models' | |||
5 | import { HttpStatusCode } from '@shared/models' | 5 | import { HttpStatusCode } from '@shared/models' |
6 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' | 6 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' |
7 | 7 | ||
8 | const videoFilesDeleteWebTorrentValidator = [ | 8 | const videoFilesDeleteWebVideoValidator = [ |
9 | isValidVideoIdParam('id'), | 9 | isValidVideoIdParam('id'), |
10 | 10 | ||
11 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 11 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
@@ -16,17 +16,17 @@ const videoFilesDeleteWebTorrentValidator = [ | |||
16 | 16 | ||
17 | if (!checkLocalVideo(video, res)) return | 17 | if (!checkLocalVideo(video, res)) return |
18 | 18 | ||
19 | if (!video.hasWebTorrentFiles()) { | 19 | if (!video.hasWebVideoFiles()) { |
20 | return res.fail({ | 20 | return res.fail({ |
21 | status: HttpStatusCode.BAD_REQUEST_400, | 21 | status: HttpStatusCode.BAD_REQUEST_400, |
22 | message: 'This video does not have WebTorrent files' | 22 | message: 'This video does not have Web Video files' |
23 | }) | 23 | }) |
24 | } | 24 | } |
25 | 25 | ||
26 | if (!video.getHLSPlaylist()) { | 26 | if (!video.getHLSPlaylist()) { |
27 | return res.fail({ | 27 | return res.fail({ |
28 | status: HttpStatusCode.BAD_REQUEST_400, | 28 | status: HttpStatusCode.BAD_REQUEST_400, |
29 | message: 'Cannot delete WebTorrent files since this video does not have HLS playlist' | 29 | message: 'Cannot delete Web Video files since this video does not have HLS playlist' |
30 | }) | 30 | }) |
31 | } | 31 | } |
32 | 32 | ||
@@ -34,7 +34,7 @@ const videoFilesDeleteWebTorrentValidator = [ | |||
34 | } | 34 | } |
35 | ] | 35 | ] |
36 | 36 | ||
37 | const videoFilesDeleteWebTorrentFileValidator = [ | 37 | const videoFilesDeleteWebVideoFileValidator = [ |
38 | isValidVideoIdParam('id'), | 38 | isValidVideoIdParam('id'), |
39 | 39 | ||
40 | param('videoFileId') | 40 | param('videoFileId') |
@@ -52,14 +52,14 @@ const videoFilesDeleteWebTorrentFileValidator = [ | |||
52 | if (!files.find(f => f.id === +req.params.videoFileId)) { | 52 | if (!files.find(f => f.id === +req.params.videoFileId)) { |
53 | return res.fail({ | 53 | return res.fail({ |
54 | status: HttpStatusCode.NOT_FOUND_404, | 54 | status: HttpStatusCode.NOT_FOUND_404, |
55 | message: 'This video does not have this WebTorrent file id' | 55 | message: 'This video does not have this Web Video file id' |
56 | }) | 56 | }) |
57 | } | 57 | } |
58 | 58 | ||
59 | if (files.length === 1 && !video.getHLSPlaylist()) { | 59 | if (files.length === 1 && !video.getHLSPlaylist()) { |
60 | return res.fail({ | 60 | return res.fail({ |
61 | status: HttpStatusCode.BAD_REQUEST_400, | 61 | status: HttpStatusCode.BAD_REQUEST_400, |
62 | message: 'Cannot delete WebTorrent files since this video does not have HLS playlist' | 62 | message: 'Cannot delete Web Video files since this video does not have HLS playlist' |
63 | }) | 63 | }) |
64 | } | 64 | } |
65 | 65 | ||
@@ -87,10 +87,10 @@ const videoFilesDeleteHLSValidator = [ | |||
87 | }) | 87 | }) |
88 | } | 88 | } |
89 | 89 | ||
90 | if (!video.hasWebTorrentFiles()) { | 90 | if (!video.hasWebVideoFiles()) { |
91 | return res.fail({ | 91 | return res.fail({ |
92 | status: HttpStatusCode.BAD_REQUEST_400, | 92 | status: HttpStatusCode.BAD_REQUEST_400, |
93 | message: 'Cannot delete HLS playlist since this video does not have WebTorrent files' | 93 | message: 'Cannot delete HLS playlist since this video does not have Web Video files' |
94 | }) | 94 | }) |
95 | } | 95 | } |
96 | 96 | ||
@@ -128,10 +128,10 @@ const videoFilesDeleteHLSFileValidator = [ | |||
128 | } | 128 | } |
129 | 129 | ||
130 | // Last file to delete | 130 | // Last file to delete |
131 | if (hlsFiles.length === 1 && !video.hasWebTorrentFiles()) { | 131 | if (hlsFiles.length === 1 && !video.hasWebVideoFiles()) { |
132 | return res.fail({ | 132 | return res.fail({ |
133 | status: HttpStatusCode.BAD_REQUEST_400, | 133 | status: HttpStatusCode.BAD_REQUEST_400, |
134 | message: 'Cannot delete last HLS playlist file since this video does not have WebTorrent files' | 134 | message: 'Cannot delete last HLS playlist file since this video does not have Web Video files' |
135 | }) | 135 | }) |
136 | } | 136 | } |
137 | 137 | ||
@@ -140,8 +140,8 @@ const videoFilesDeleteHLSFileValidator = [ | |||
140 | ] | 140 | ] |
141 | 141 | ||
142 | export { | 142 | export { |
143 | videoFilesDeleteWebTorrentValidator, | 143 | videoFilesDeleteWebVideoValidator, |
144 | videoFilesDeleteWebTorrentFileValidator, | 144 | videoFilesDeleteWebVideoFileValidator, |
145 | 145 | ||
146 | videoFilesDeleteHLSValidator, | 146 | videoFilesDeleteHLSValidator, |
147 | videoFilesDeleteHLSFileValidator | 147 | videoFilesDeleteHLSFileValidator |
diff --git a/server/middlewares/validators/videos/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts index 72442aeb6..a1cb65b70 100644 --- a/server/middlewares/validators/videos/video-imports.ts +++ b/server/middlewares/validators/videos/video-imports.ts | |||
@@ -9,7 +9,11 @@ import { HttpStatusCode, UserRight, VideoImportState } from '@shared/models' | |||
9 | import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model' | 9 | import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model' |
10 | import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' | 10 | import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' |
11 | import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' | 11 | import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' |
12 | import { isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos' | 12 | import { |
13 | isValidPasswordProtectedPrivacy, | ||
14 | isVideoMagnetUriValid, | ||
15 | isVideoNameValid | ||
16 | } from '../../../helpers/custom-validators/videos' | ||
13 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 17 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
14 | import { logger } from '../../../helpers/logger' | 18 | import { logger } from '../../../helpers/logger' |
15 | import { CONFIG } from '../../../initializers/config' | 19 | import { CONFIG } from '../../../initializers/config' |
@@ -38,6 +42,10 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([ | |||
38 | .custom(isVideoNameValid).withMessage( | 42 | .custom(isVideoNameValid).withMessage( |
39 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` | 43 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` |
40 | ), | 44 | ), |
45 | body('videoPasswords') | ||
46 | .optional() | ||
47 | .isArray() | ||
48 | .withMessage('Video passwords should be an array.'), | ||
41 | 49 | ||
42 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 50 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
43 | const user = res.locals.oauth.token.User | 51 | const user = res.locals.oauth.token.User |
@@ -45,6 +53,8 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([ | |||
45 | 53 | ||
46 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | 54 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) |
47 | 55 | ||
56 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req) | ||
57 | |||
48 | if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) { | 58 | if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) { |
49 | cleanUpReqFiles(req) | 59 | cleanUpReqFiles(req) |
50 | 60 | ||
diff --git a/server/middlewares/validators/videos/video-live.ts b/server/middlewares/validators/videos/video-live.ts index 2aff831a8..ec69a3011 100644 --- a/server/middlewares/validators/videos/video-live.ts +++ b/server/middlewares/validators/videos/video-live.ts | |||
@@ -17,7 +17,7 @@ import { | |||
17 | VideoState | 17 | VideoState |
18 | } from '@shared/models' | 18 | } from '@shared/models' |
19 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' | 19 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' |
20 | import { isVideoNameValid, isVideoPrivacyValid } from '../../../helpers/custom-validators/videos' | 20 | import { isValidPasswordProtectedPrivacy, isVideoNameValid, isVideoReplayPrivacyValid } from '../../../helpers/custom-validators/videos' |
21 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 21 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
22 | import { logger } from '../../../helpers/logger' | 22 | import { logger } from '../../../helpers/logger' |
23 | import { CONFIG } from '../../../initializers/config' | 23 | import { CONFIG } from '../../../initializers/config' |
@@ -69,7 +69,7 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
69 | body('replaySettings.privacy') | 69 | body('replaySettings.privacy') |
70 | .optional() | 70 | .optional() |
71 | .customSanitizer(toIntOrNull) | 71 | .customSanitizer(toIntOrNull) |
72 | .custom(isVideoPrivacyValid), | 72 | .custom(isVideoReplayPrivacyValid), |
73 | 73 | ||
74 | body('permanentLive') | 74 | body('permanentLive') |
75 | .optional() | 75 | .optional() |
@@ -81,9 +81,16 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
81 | .customSanitizer(toIntOrNull) | 81 | .customSanitizer(toIntOrNull) |
82 | .custom(isLiveLatencyModeValid), | 82 | .custom(isLiveLatencyModeValid), |
83 | 83 | ||
84 | body('videoPasswords') | ||
85 | .optional() | ||
86 | .isArray() | ||
87 | .withMessage('Video passwords should be an array.'), | ||
88 | |||
84 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 89 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
85 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | 90 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) |
86 | 91 | ||
92 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req) | ||
93 | |||
87 | if (CONFIG.LIVE.ENABLED !== true) { | 94 | if (CONFIG.LIVE.ENABLED !== true) { |
88 | cleanUpReqFiles(req) | 95 | cleanUpReqFiles(req) |
89 | 96 | ||
@@ -170,7 +177,7 @@ const videoLiveUpdateValidator = [ | |||
170 | body('replaySettings.privacy') | 177 | body('replaySettings.privacy') |
171 | .optional() | 178 | .optional() |
172 | .customSanitizer(toIntOrNull) | 179 | .customSanitizer(toIntOrNull) |
173 | .custom(isVideoPrivacyValid), | 180 | .custom(isVideoReplayPrivacyValid), |
174 | 181 | ||
175 | body('latencyMode') | 182 | body('latencyMode') |
176 | .optional() | 183 | .optional() |
diff --git a/server/middlewares/validators/videos/video-passwords.ts b/server/middlewares/validators/videos/video-passwords.ts new file mode 100644 index 000000000..200e496f6 --- /dev/null +++ b/server/middlewares/validators/videos/video-passwords.ts | |||
@@ -0,0 +1,77 @@ | |||
1 | import express from 'express' | ||
2 | import { | ||
3 | areValidationErrors, | ||
4 | doesVideoExist, | ||
5 | isVideoPasswordProtected, | ||
6 | isValidVideoIdParam, | ||
7 | doesVideoPasswordExist, | ||
8 | isVideoPasswordDeletable, | ||
9 | checkUserCanManageVideo | ||
10 | } from '../shared' | ||
11 | import { body, param } from 'express-validator' | ||
12 | import { isIdValid } from '@server/helpers/custom-validators/misc' | ||
13 | import { isValidPasswordProtectedPrivacy } from '@server/helpers/custom-validators/videos' | ||
14 | import { UserRight } from '@shared/models' | ||
15 | |||
16 | const listVideoPasswordValidator = [ | ||
17 | isValidVideoIdParam('videoId'), | ||
18 | |||
19 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
20 | if (areValidationErrors(req, res)) return | ||
21 | |||
22 | if (!await doesVideoExist(req.params.videoId, res)) return | ||
23 | if (!isVideoPasswordProtected(res)) return | ||
24 | |||
25 | // Check if the user who did the request is able to access video password list | ||
26 | const user = res.locals.oauth.token.User | ||
27 | if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.SEE_ALL_VIDEOS, res)) return | ||
28 | |||
29 | return next() | ||
30 | } | ||
31 | ] | ||
32 | |||
33 | const updateVideoPasswordListValidator = [ | ||
34 | body('passwords') | ||
35 | .optional() | ||
36 | .isArray() | ||
37 | .withMessage('Video passwords should be an array.'), | ||
38 | |||
39 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
40 | if (areValidationErrors(req, res)) return | ||
41 | |||
42 | if (!await doesVideoExist(req.params.videoId, res)) return | ||
43 | if (!isValidPasswordProtectedPrivacy(req, res)) return | ||
44 | |||
45 | // Check if the user who did the request is able to update video passwords | ||
46 | const user = res.locals.oauth.token.User | ||
47 | if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return | ||
48 | |||
49 | return next() | ||
50 | } | ||
51 | ] | ||
52 | |||
53 | const removeVideoPasswordValidator = [ | ||
54 | isValidVideoIdParam('videoId'), | ||
55 | |||
56 | param('passwordId') | ||
57 | .custom(isIdValid), | ||
58 | |||
59 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
60 | if (areValidationErrors(req, res)) return | ||
61 | |||
62 | if (!await doesVideoExist(req.params.videoId, res)) return | ||
63 | if (!isVideoPasswordProtected(res)) return | ||
64 | if (!await doesVideoPasswordExist(req.params.passwordId, res)) return | ||
65 | if (!await isVideoPasswordDeletable(res)) return | ||
66 | |||
67 | return next() | ||
68 | } | ||
69 | ] | ||
70 | |||
71 | // --------------------------------------------------------------------------- | ||
72 | |||
73 | export { | ||
74 | listVideoPasswordValidator, | ||
75 | updateVideoPasswordListValidator, | ||
76 | removeVideoPasswordValidator | ||
77 | } | ||
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts index c631a16f8..95a5ba63a 100644 --- a/server/middlewares/validators/videos/video-playlists.ts +++ b/server/middlewares/validators/videos/video-playlists.ts | |||
@@ -153,7 +153,7 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => { | |||
153 | } | 153 | } |
154 | 154 | ||
155 | if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { | 155 | if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { |
156 | await authenticatePromise(req, res) | 156 | await authenticatePromise({ req, res }) |
157 | 157 | ||
158 | const user = res.locals.oauth ? res.locals.oauth.token.User : null | 158 | const user = res.locals.oauth ? res.locals.oauth.token.User : null |
159 | 159 | ||
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts index 275634d5b..c837b047b 100644 --- a/server/middlewares/validators/videos/video-rates.ts +++ b/server/middlewares/validators/videos/video-rates.ts | |||
@@ -7,13 +7,14 @@ import { isIdValid } from '../../../helpers/custom-validators/misc' | |||
7 | import { isRatingValid } from '../../../helpers/custom-validators/video-rates' | 7 | import { isRatingValid } from '../../../helpers/custom-validators/video-rates' |
8 | import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' | 8 | import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' |
9 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 9 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
10 | import { areValidationErrors, checkCanSeeVideo, doesVideoExist, isValidVideoIdParam } from '../shared' | 10 | import { areValidationErrors, checkCanSeeVideo, doesVideoExist, isValidVideoIdParam, isValidVideoPasswordHeader } from '../shared' |
11 | 11 | ||
12 | const videoUpdateRateValidator = [ | 12 | const videoUpdateRateValidator = [ |
13 | isValidVideoIdParam('id'), | 13 | isValidVideoIdParam('id'), |
14 | 14 | ||
15 | body('rating') | 15 | body('rating') |
16 | .custom(isVideoRatingTypeValid), | 16 | .custom(isVideoRatingTypeValid), |
17 | isValidVideoPasswordHeader(), | ||
17 | 18 | ||
18 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 19 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
19 | if (areValidationErrors(req, res)) return | 20 | if (areValidationErrors(req, res)) return |
diff --git a/server/middlewares/validators/videos/video-token.ts b/server/middlewares/validators/videos/video-token.ts new file mode 100644 index 000000000..d4253e21d --- /dev/null +++ b/server/middlewares/validators/videos/video-token.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import express from 'express' | ||
2 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
3 | import { HttpStatusCode } from '@shared/models' | ||
4 | import { exists } from '@server/helpers/custom-validators/misc' | ||
5 | |||
6 | const videoFileTokenValidator = [ | ||
7 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
8 | const video = res.locals.onlyVideo | ||
9 | if (video.privacy !== VideoPrivacy.PASSWORD_PROTECTED && !exists(res.locals.oauth.token.User)) { | ||
10 | return res.fail({ | ||
11 | status: HttpStatusCode.UNAUTHORIZED_401, | ||
12 | message: 'Not authenticated' | ||
13 | }) | ||
14 | } | ||
15 | |||
16 | return next() | ||
17 | } | ||
18 | ] | ||
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | export { | ||
23 | videoFileTokenValidator | ||
24 | } | ||
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 794e1d4f1..b39d13a23 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -2,6 +2,7 @@ import express from 'express' | |||
2 | import { body, header, param, query, ValidationChain } from 'express-validator' | 2 | import { body, header, param, query, ValidationChain } from 'express-validator' |
3 | import { isTestInstance } from '@server/helpers/core-utils' | 3 | import { isTestInstance } from '@server/helpers/core-utils' |
4 | import { getResumableUploadPath } from '@server/helpers/upload' | 4 | import { getResumableUploadPath } from '@server/helpers/upload' |
5 | import { uploadx } from '@server/lib/uploadx' | ||
5 | import { Redis } from '@server/lib/redis' | 6 | import { Redis } from '@server/lib/redis' |
6 | import { getServerActor } from '@server/models/application/application' | 7 | import { getServerActor } from '@server/models/application/application' |
7 | import { ExpressPromiseHandler } from '@server/types/express-handler' | 8 | import { ExpressPromiseHandler } from '@server/types/express-handler' |
@@ -23,6 +24,7 @@ import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../ | |||
23 | import { | 24 | import { |
24 | areVideoTagsValid, | 25 | areVideoTagsValid, |
25 | isScheduleVideoUpdatePrivacyValid, | 26 | isScheduleVideoUpdatePrivacyValid, |
27 | isValidPasswordProtectedPrivacy, | ||
26 | isVideoCategoryValid, | 28 | isVideoCategoryValid, |
27 | isVideoDescriptionValid, | 29 | isVideoDescriptionValid, |
28 | isVideoFileMimeTypeValid, | 30 | isVideoFileMimeTypeValid, |
@@ -39,7 +41,6 @@ import { | |||
39 | } from '../../../helpers/custom-validators/videos' | 41 | } from '../../../helpers/custom-validators/videos' |
40 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 42 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
41 | import { logger } from '../../../helpers/logger' | 43 | import { logger } from '../../../helpers/logger' |
42 | import { deleteFileAndCatch } from '../../../helpers/utils' | ||
43 | import { getVideoWithAttributes } from '../../../helpers/video' | 44 | import { getVideoWithAttributes } from '../../../helpers/video' |
44 | import { CONFIG } from '../../../initializers/config' | 45 | import { CONFIG } from '../../../initializers/config' |
45 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' | 46 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' |
@@ -55,7 +56,8 @@ import { | |||
55 | doesVideoChannelOfAccountExist, | 56 | doesVideoChannelOfAccountExist, |
56 | doesVideoExist, | 57 | doesVideoExist, |
57 | doesVideoFileOfVideoExist, | 58 | doesVideoFileOfVideoExist, |
58 | isValidVideoIdParam | 59 | isValidVideoIdParam, |
60 | isValidVideoPasswordHeader | ||
59 | } from '../shared' | 61 | } from '../shared' |
60 | 62 | ||
61 | const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | 63 | const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ |
@@ -70,6 +72,10 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | |||
70 | body('channelId') | 72 | body('channelId') |
71 | .customSanitizer(toIntOrNull) | 73 | .customSanitizer(toIntOrNull) |
72 | .custom(isIdValid), | 74 | .custom(isIdValid), |
75 | body('videoPasswords') | ||
76 | .optional() | ||
77 | .isArray() | ||
78 | .withMessage('Video passwords should be an array.'), | ||
73 | 79 | ||
74 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 80 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
75 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | 81 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) |
@@ -81,6 +87,8 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | |||
81 | return cleanUpReqFiles(req) | 87 | return cleanUpReqFiles(req) |
82 | } | 88 | } |
83 | 89 | ||
90 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req) | ||
91 | |||
84 | try { | 92 | try { |
85 | if (!videoFile.duration) await addDurationToVideo(videoFile) | 93 | if (!videoFile.duration) await addDurationToVideo(videoFile) |
86 | } catch (err) { | 94 | } catch (err) { |
@@ -107,7 +115,7 @@ const videosAddResumableValidator = [ | |||
107 | const user = res.locals.oauth.token.User | 115 | const user = res.locals.oauth.token.User |
108 | const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body | 116 | const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body |
109 | const file = { ...body, duration: undefined, path: getResumableUploadPath(body.name), filename: body.metadata.filename } | 117 | const file = { ...body, duration: undefined, path: getResumableUploadPath(body.name), filename: body.metadata.filename } |
110 | const cleanup = () => deleteFileAndCatch(file.path) | 118 | const cleanup = () => uploadx.storage.delete(file).catch(err => logger.error('Cannot delete the file %s', file.name, { err })) |
111 | 119 | ||
112 | const uploadId = req.query.upload_id | 120 | const uploadId = req.query.upload_id |
113 | const sessionExists = await Redis.Instance.doesUploadSessionExist(uploadId) | 121 | const sessionExists = await Redis.Instance.doesUploadSessionExist(uploadId) |
@@ -124,11 +132,15 @@ const videosAddResumableValidator = [ | |||
124 | }) | 132 | }) |
125 | } | 133 | } |
126 | 134 | ||
127 | if (isTestInstance()) { | 135 | const videoStillExists = await VideoModel.load(sessionResponse.video.id) |
128 | res.setHeader('x-resumable-upload-cached', 'true') | 136 | |
129 | } | 137 | if (videoStillExists) { |
138 | if (isTestInstance()) { | ||
139 | res.setHeader('x-resumable-upload-cached', 'true') | ||
140 | } | ||
130 | 141 | ||
131 | return res.json(sessionResponse) | 142 | return res.json(sessionResponse) |
143 | } | ||
132 | } | 144 | } |
133 | 145 | ||
134 | await Redis.Instance.setUploadSession(uploadId) | 146 | await Redis.Instance.setUploadSession(uploadId) |
@@ -174,6 +186,10 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([ | |||
174 | body('channelId') | 186 | body('channelId') |
175 | .customSanitizer(toIntOrNull) | 187 | .customSanitizer(toIntOrNull) |
176 | .custom(isIdValid), | 188 | .custom(isIdValid), |
189 | body('videoPasswords') | ||
190 | .optional() | ||
191 | .isArray() | ||
192 | .withMessage('Video passwords should be an array.'), | ||
177 | 193 | ||
178 | header('x-upload-content-length') | 194 | header('x-upload-content-length') |
179 | .isNumeric() | 195 | .isNumeric() |
@@ -205,10 +221,14 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([ | |||
205 | const files = { videofile: [ videoFileMetadata ] } | 221 | const files = { videofile: [ videoFileMetadata ] } |
206 | if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup() | 222 | if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup() |
207 | 223 | ||
208 | // multer required unsetting the Content-Type, now we can set it for node-uploadx | 224 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanup() |
225 | |||
226 | // Multer required unsetting the Content-Type, now we can set it for node-uploadx | ||
209 | req.headers['content-type'] = 'application/json; charset=utf-8' | 227 | req.headers['content-type'] = 'application/json; charset=utf-8' |
210 | // place previewfile in metadata so that uploadx saves it in .META | 228 | |
229 | // Place thumbnail/previewfile in metadata so that uploadx saves it in .META | ||
211 | if (req.files?.['previewfile']) req.body.previewfile = req.files['previewfile'] | 230 | if (req.files?.['previewfile']) req.body.previewfile = req.files['previewfile'] |
231 | if (req.files?.['thumbnailfile']) req.body.thumbnailfile = req.files['thumbnailfile'] | ||
212 | 232 | ||
213 | return next() | 233 | return next() |
214 | } | 234 | } |
@@ -227,12 +247,18 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([ | |||
227 | .optional() | 247 | .optional() |
228 | .customSanitizer(toIntOrNull) | 248 | .customSanitizer(toIntOrNull) |
229 | .custom(isIdValid), | 249 | .custom(isIdValid), |
250 | body('videoPasswords') | ||
251 | .optional() | ||
252 | .isArray() | ||
253 | .withMessage('Video passwords should be an array.'), | ||
230 | 254 | ||
231 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 255 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
232 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | 256 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) |
233 | if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) | 257 | if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) |
234 | if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req) | 258 | if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req) |
235 | 259 | ||
260 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req) | ||
261 | |||
236 | const video = getVideoWithAttributes(res) | 262 | const video = getVideoWithAttributes(res) |
237 | if (video.isLive && video.privacy !== req.body.privacy && video.state !== VideoState.WAITING_FOR_LIVE) { | 263 | if (video.isLive && video.privacy !== req.body.privacy && video.state !== VideoState.WAITING_FOR_LIVE) { |
238 | return res.fail({ message: 'Cannot update privacy of a live that has already started' }) | 264 | return res.fail({ message: 'Cannot update privacy of a live that has already started' }) |
@@ -281,6 +307,8 @@ const videosCustomGetValidator = (fetchType: 'for-api' | 'all' | 'only-video' | | |||
281 | return [ | 307 | return [ |
282 | isValidVideoIdParam('id'), | 308 | isValidVideoIdParam('id'), |
283 | 309 | ||
310 | isValidVideoPasswordHeader(), | ||
311 | |||
284 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 312 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
285 | if (areValidationErrors(req, res)) return | 313 | if (areValidationErrors(req, res)) return |
286 | if (!await doesVideoExist(req.params.id, res, fetchType)) return | 314 | if (!await doesVideoExist(req.params.id, res, fetchType)) return |
@@ -478,10 +506,14 @@ const commonVideosFiltersValidator = [ | |||
478 | .optional() | 506 | .optional() |
479 | .customSanitizer(toBooleanOrNull) | 507 | .customSanitizer(toBooleanOrNull) |
480 | .custom(isBooleanValid).withMessage('Should have a valid hasHLSFiles boolean'), | 508 | .custom(isBooleanValid).withMessage('Should have a valid hasHLSFiles boolean'), |
481 | query('hasWebtorrentFiles') | 509 | query('hasWebtorrentFiles') // TODO: remove in v7 |
482 | .optional() | 510 | .optional() |
483 | .customSanitizer(toBooleanOrNull) | 511 | .customSanitizer(toBooleanOrNull) |
484 | .custom(isBooleanValid).withMessage('Should have a valid hasWebtorrentFiles boolean'), | 512 | .custom(isBooleanValid).withMessage('Should have a valid hasWebtorrentFiles boolean'), |
513 | query('hasWebVideoFiles') | ||
514 | .optional() | ||
515 | .customSanitizer(toBooleanOrNull) | ||
516 | .custom(isBooleanValid).withMessage('Should have a valid hasWebVideoFiles boolean'), | ||
485 | query('skipCount') | 517 | query('skipCount') |
486 | .optional() | 518 | .optional() |
487 | .customSanitizer(toBooleanOrNull) | 519 | .customSanitizer(toBooleanOrNull) |
diff --git a/server/models/actor/actor-image.ts b/server/models/actor/actor-image.ts index 9c34a0101..51085a16d 100644 --- a/server/models/actor/actor-image.ts +++ b/server/models/actor/actor-image.ts | |||
@@ -157,11 +157,11 @@ export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageMode | |||
157 | } | 157 | } |
158 | 158 | ||
159 | getPath () { | 159 | getPath () { |
160 | return join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) | 160 | return join(CONFIG.STORAGE.ACTOR_IMAGES_DIR, this.filename) |
161 | } | 161 | } |
162 | 162 | ||
163 | removeImage () { | 163 | removeImage () { |
164 | const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) | 164 | const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES_DIR, this.filename) |
165 | return remove(imagePath) | 165 | return remove(imagePath) |
166 | } | 166 | } |
167 | 167 | ||
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index c2a72b71f..cebf47dfd 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -162,7 +162,7 @@ export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedu | |||
162 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` | 162 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` |
163 | logger.info('Removing duplicated video file %s.', logIdentifier) | 163 | logger.info('Removing duplicated video file %s.', logIdentifier) |
164 | 164 | ||
165 | videoFile.Video.removeWebTorrentFile(videoFile, true) | 165 | videoFile.Video.removeWebVideoFile(videoFile, true) |
166 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) | 166 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) |
167 | } | 167 | } |
168 | 168 | ||
diff --git a/server/models/user/user.ts b/server/models/user/user.ts index 4f6a8fce4..ff6328d48 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts | |||
@@ -786,7 +786,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
786 | 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + | 786 | 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + |
787 | `WHERE "account"."userId" = ${options.whereUserId} ${andWhere}` | 787 | `WHERE "account"."userId" = ${options.whereUserId} ${andWhere}` |
788 | 788 | ||
789 | const webtorrentFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' + | 789 | const webVideoFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' + |
790 | 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" AND "video"."isLive" IS FALSE ' + | 790 | 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" AND "video"."isLive" IS FALSE ' + |
791 | videoChannelJoin | 791 | videoChannelJoin |
792 | 792 | ||
@@ -797,7 +797,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
797 | 797 | ||
798 | return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' + | 798 | return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' + |
799 | 'FROM (' + | 799 | 'FROM (' + |
800 | `SELECT MAX("t1"."size") AS "size" FROM (${webtorrentFiles} UNION ${hlsFiles}) t1 ` + | 800 | `SELECT MAX("t1"."size") AS "size" FROM (${webVideoFiles} UNION ${hlsFiles}) t1 ` + |
801 | 'GROUP BY "t1"."videoId"' + | 801 | 'GROUP BY "t1"."videoId"' + |
802 | ') t2' | 802 | ') t2' |
803 | } | 803 | } |
@@ -890,8 +890,6 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
890 | 890 | ||
891 | nsfwPolicy: this.nsfwPolicy, | 891 | nsfwPolicy: this.nsfwPolicy, |
892 | 892 | ||
893 | // FIXME: deprecated in 4.1 | ||
894 | webTorrentEnabled: this.p2pEnabled, | ||
895 | p2pEnabled: this.p2pEnabled, | 893 | p2pEnabled: this.p2pEnabled, |
896 | 894 | ||
897 | videosHistoryEnabled: this.videosHistoryEnabled, | 895 | videosHistoryEnabled: this.videosHistoryEnabled, |
diff --git a/server/models/video/formatter/index.ts b/server/models/video/formatter/index.ts new file mode 100644 index 000000000..77b406559 --- /dev/null +++ b/server/models/video/formatter/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './video-activity-pub-format' | ||
2 | export * from './video-api-format' | ||
diff --git a/server/models/video/formatter/shared/index.ts b/server/models/video/formatter/shared/index.ts new file mode 100644 index 000000000..d558fa7d6 --- /dev/null +++ b/server/models/video/formatter/shared/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './video-format-utils' | |||
diff --git a/server/models/video/formatter/shared/video-format-utils.ts b/server/models/video/formatter/shared/video-format-utils.ts new file mode 100644 index 000000000..df3bbdf1c --- /dev/null +++ b/server/models/video/formatter/shared/video-format-utils.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | import { MVideoFile } from '@server/types/models' | ||
2 | |||
3 | export function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) { | ||
4 | if (fileA.resolution < fileB.resolution) return 1 | ||
5 | if (fileA.resolution === fileB.resolution) return 0 | ||
6 | return -1 | ||
7 | } | ||
diff --git a/server/models/video/formatter/video-activity-pub-format.ts b/server/models/video/formatter/video-activity-pub-format.ts new file mode 100644 index 000000000..c0d3d5f3e --- /dev/null +++ b/server/models/video/formatter/video-activity-pub-format.ts | |||
@@ -0,0 +1,295 @@ | |||
1 | |||
2 | import { isArray } from 'lodash' | ||
3 | import { generateMagnetUri } from '@server/helpers/webtorrent' | ||
4 | import { getActivityStreamDuration } from '@server/lib/activitypub/activity' | ||
5 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls' | ||
6 | import { | ||
7 | ActivityIconObject, | ||
8 | ActivityPlaylistUrlObject, | ||
9 | ActivityPubStoryboard, | ||
10 | ActivityTagObject, | ||
11 | ActivityTrackerUrlObject, | ||
12 | ActivityUrlObject, | ||
13 | VideoObject | ||
14 | } from '@shared/models' | ||
15 | import { MIMETYPES, WEBSERVER } from '../../../initializers/constants' | ||
16 | import { | ||
17 | getLocalVideoCommentsActivityPubUrl, | ||
18 | getLocalVideoDislikesActivityPubUrl, | ||
19 | getLocalVideoLikesActivityPubUrl, | ||
20 | getLocalVideoSharesActivityPubUrl | ||
21 | } from '../../../lib/activitypub/url' | ||
22 | import { MStreamingPlaylistFiles, MUserId, MVideo, MVideoAP, MVideoFile } from '../../../types/models' | ||
23 | import { VideoCaptionModel } from '../video-caption' | ||
24 | import { sortByResolutionDesc } from './shared' | ||
25 | import { getCategoryLabel, getLanguageLabel, getLicenceLabel } from './video-api-format' | ||
26 | |||
27 | export function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | ||
28 | const language = video.language | ||
29 | ? { identifier: video.language, name: getLanguageLabel(video.language) } | ||
30 | : undefined | ||
31 | |||
32 | const category = video.category | ||
33 | ? { identifier: video.category + '', name: getCategoryLabel(video.category) } | ||
34 | : undefined | ||
35 | |||
36 | const licence = video.licence | ||
37 | ? { identifier: video.licence + '', name: getLicenceLabel(video.licence) } | ||
38 | : undefined | ||
39 | |||
40 | const url: ActivityUrlObject[] = [ | ||
41 | // HTML url should be the first element in the array so Mastodon correctly displays the embed | ||
42 | { | ||
43 | type: 'Link', | ||
44 | mediaType: 'text/html', | ||
45 | href: WEBSERVER.URL + '/videos/watch/' + video.uuid | ||
46 | } as ActivityUrlObject, | ||
47 | |||
48 | ...buildVideoFileUrls({ video, files: video.VideoFiles }), | ||
49 | |||
50 | ...buildStreamingPlaylistUrls(video), | ||
51 | |||
52 | ...buildTrackerUrls(video) | ||
53 | ] | ||
54 | |||
55 | return { | ||
56 | type: 'Video' as 'Video', | ||
57 | id: video.url, | ||
58 | name: video.name, | ||
59 | duration: getActivityStreamDuration(video.duration), | ||
60 | uuid: video.uuid, | ||
61 | category, | ||
62 | licence, | ||
63 | language, | ||
64 | views: video.views, | ||
65 | sensitive: video.nsfw, | ||
66 | waitTranscoding: video.waitTranscoding, | ||
67 | |||
68 | state: video.state, | ||
69 | commentsEnabled: video.commentsEnabled, | ||
70 | downloadEnabled: video.downloadEnabled, | ||
71 | published: video.publishedAt.toISOString(), | ||
72 | |||
73 | originallyPublishedAt: video.originallyPublishedAt | ||
74 | ? video.originallyPublishedAt.toISOString() | ||
75 | : null, | ||
76 | |||
77 | updated: video.updatedAt.toISOString(), | ||
78 | |||
79 | tag: buildTags(video), | ||
80 | |||
81 | mediaType: 'text/markdown', | ||
82 | content: video.description, | ||
83 | support: video.support, | ||
84 | |||
85 | subtitleLanguage: buildSubtitleLanguage(video), | ||
86 | |||
87 | icon: buildIcon(video), | ||
88 | |||
89 | preview: buildPreviewAPAttribute(video), | ||
90 | |||
91 | url, | ||
92 | |||
93 | likes: getLocalVideoLikesActivityPubUrl(video), | ||
94 | dislikes: getLocalVideoDislikesActivityPubUrl(video), | ||
95 | shares: getLocalVideoSharesActivityPubUrl(video), | ||
96 | comments: getLocalVideoCommentsActivityPubUrl(video), | ||
97 | |||
98 | attributedTo: [ | ||
99 | { | ||
100 | type: 'Person', | ||
101 | id: video.VideoChannel.Account.Actor.url | ||
102 | }, | ||
103 | { | ||
104 | type: 'Group', | ||
105 | id: video.VideoChannel.Actor.url | ||
106 | } | ||
107 | ], | ||
108 | |||
109 | ...buildLiveAPAttributes(video) | ||
110 | } | ||
111 | } | ||
112 | |||
113 | // --------------------------------------------------------------------------- | ||
114 | // Private | ||
115 | // --------------------------------------------------------------------------- | ||
116 | |||
117 | function buildLiveAPAttributes (video: MVideoAP) { | ||
118 | if (!video.isLive) { | ||
119 | return { | ||
120 | isLiveBroadcast: false, | ||
121 | liveSaveReplay: null, | ||
122 | permanentLive: null, | ||
123 | latencyMode: null | ||
124 | } | ||
125 | } | ||
126 | |||
127 | return { | ||
128 | isLiveBroadcast: true, | ||
129 | liveSaveReplay: video.VideoLive.saveReplay, | ||
130 | permanentLive: video.VideoLive.permanentLive, | ||
131 | latencyMode: video.VideoLive.latencyMode | ||
132 | } | ||
133 | } | ||
134 | |||
135 | function buildPreviewAPAttribute (video: MVideoAP): ActivityPubStoryboard[] { | ||
136 | if (!video.Storyboard) return undefined | ||
137 | |||
138 | const storyboard = video.Storyboard | ||
139 | |||
140 | return [ | ||
141 | { | ||
142 | type: 'Image', | ||
143 | rel: [ 'storyboard' ], | ||
144 | url: [ | ||
145 | { | ||
146 | mediaType: 'image/jpeg', | ||
147 | |||
148 | href: storyboard.getOriginFileUrl(video), | ||
149 | |||
150 | width: storyboard.totalWidth, | ||
151 | height: storyboard.totalHeight, | ||
152 | |||
153 | tileWidth: storyboard.spriteWidth, | ||
154 | tileHeight: storyboard.spriteHeight, | ||
155 | tileDuration: getActivityStreamDuration(storyboard.spriteDuration) | ||
156 | } | ||
157 | ] | ||
158 | } | ||
159 | ] | ||
160 | } | ||
161 | |||
162 | function buildVideoFileUrls (options: { | ||
163 | video: MVideo | ||
164 | files: MVideoFile[] | ||
165 | user?: MUserId | ||
166 | }): ActivityUrlObject[] { | ||
167 | const { video, files } = options | ||
168 | |||
169 | if (!isArray(files)) return [] | ||
170 | |||
171 | const urls: ActivityUrlObject[] = [] | ||
172 | |||
173 | const trackerUrls = video.getTrackerUrls() | ||
174 | const sortedFiles = files | ||
175 | .filter(f => !f.isLive()) | ||
176 | .sort(sortByResolutionDesc) | ||
177 | |||
178 | for (const file of sortedFiles) { | ||
179 | urls.push({ | ||
180 | type: 'Link', | ||
181 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any, | ||
182 | href: file.getFileUrl(video), | ||
183 | height: file.resolution, | ||
184 | size: file.size, | ||
185 | fps: file.fps | ||
186 | }) | ||
187 | |||
188 | urls.push({ | ||
189 | type: 'Link', | ||
190 | rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ], | ||
191 | mediaType: 'application/json' as 'application/json', | ||
192 | href: getLocalVideoFileMetadataUrl(video, file), | ||
193 | height: file.resolution, | ||
194 | fps: file.fps | ||
195 | }) | ||
196 | |||
197 | if (file.hasTorrent()) { | ||
198 | urls.push({ | ||
199 | type: 'Link', | ||
200 | mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
201 | href: file.getTorrentUrl(), | ||
202 | height: file.resolution | ||
203 | }) | ||
204 | |||
205 | urls.push({ | ||
206 | type: 'Link', | ||
207 | mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
208 | href: generateMagnetUri(video, file, trackerUrls), | ||
209 | height: file.resolution | ||
210 | }) | ||
211 | } | ||
212 | } | ||
213 | |||
214 | return urls | ||
215 | } | ||
216 | |||
217 | // --------------------------------------------------------------------------- | ||
218 | |||
219 | function buildStreamingPlaylistUrls (video: MVideoAP): ActivityPlaylistUrlObject[] { | ||
220 | if (!isArray(video.VideoStreamingPlaylists)) return [] | ||
221 | |||
222 | return video.VideoStreamingPlaylists | ||
223 | .map(playlist => ({ | ||
224 | type: 'Link', | ||
225 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', | ||
226 | href: playlist.getMasterPlaylistUrl(video), | ||
227 | tag: buildStreamingPlaylistTags(video, playlist) | ||
228 | })) | ||
229 | } | ||
230 | |||
231 | function buildStreamingPlaylistTags (video: MVideoAP, playlist: MStreamingPlaylistFiles) { | ||
232 | return [ | ||
233 | ...playlist.p2pMediaLoaderInfohashes.map(i => ({ type: 'Infohash' as 'Infohash', name: i })), | ||
234 | |||
235 | { | ||
236 | type: 'Link', | ||
237 | name: 'sha256', | ||
238 | mediaType: 'application/json' as 'application/json', | ||
239 | href: playlist.getSha256SegmentsUrl(video) | ||
240 | }, | ||
241 | |||
242 | ...buildVideoFileUrls({ video, files: playlist.VideoFiles }) | ||
243 | ] as ActivityTagObject[] | ||
244 | } | ||
245 | |||
246 | // --------------------------------------------------------------------------- | ||
247 | |||
248 | function buildTrackerUrls (video: MVideoAP): ActivityTrackerUrlObject[] { | ||
249 | return video.getTrackerUrls() | ||
250 | .map(trackerUrl => { | ||
251 | const rel2 = trackerUrl.startsWith('http') | ||
252 | ? 'http' | ||
253 | : 'websocket' | ||
254 | |||
255 | return { | ||
256 | type: 'Link', | ||
257 | name: `tracker-${rel2}`, | ||
258 | rel: [ 'tracker', rel2 ], | ||
259 | href: trackerUrl | ||
260 | } | ||
261 | }) | ||
262 | } | ||
263 | |||
264 | // --------------------------------------------------------------------------- | ||
265 | |||
266 | function buildTags (video: MVideoAP) { | ||
267 | if (!isArray(video.Tags)) return [] | ||
268 | |||
269 | return video.Tags.map(t => ({ | ||
270 | type: 'Hashtag' as 'Hashtag', | ||
271 | name: t.name | ||
272 | })) | ||
273 | } | ||
274 | |||
275 | function buildIcon (video: MVideoAP): ActivityIconObject[] { | ||
276 | return [ video.getMiniature(), video.getPreview() ] | ||
277 | .map(i => ({ | ||
278 | type: 'Image', | ||
279 | url: i.getOriginFileUrl(video), | ||
280 | mediaType: 'image/jpeg', | ||
281 | width: i.width, | ||
282 | height: i.height | ||
283 | })) | ||
284 | } | ||
285 | |||
286 | function buildSubtitleLanguage (video: MVideoAP) { | ||
287 | if (!isArray(video.VideoCaptions)) return [] | ||
288 | |||
289 | return video.VideoCaptions | ||
290 | .map(caption => ({ | ||
291 | identifier: caption.language, | ||
292 | name: VideoCaptionModel.getLanguageLabel(caption.language), | ||
293 | url: caption.getFileUrl(video) | ||
294 | })) | ||
295 | } | ||
diff --git a/server/models/video/formatter/video-api-format.ts b/server/models/video/formatter/video-api-format.ts new file mode 100644 index 000000000..1af51d132 --- /dev/null +++ b/server/models/video/formatter/video-api-format.ts | |||
@@ -0,0 +1,304 @@ | |||
1 | import { generateMagnetUri } from '@server/helpers/webtorrent' | ||
2 | import { tracer } from '@server/lib/opentelemetry/tracing' | ||
3 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls' | ||
4 | import { VideoViewsManager } from '@server/lib/views/video-views-manager' | ||
5 | import { uuidToShort } from '@shared/extra-utils' | ||
6 | import { | ||
7 | Video, | ||
8 | VideoAdditionalAttributes, | ||
9 | VideoDetails, | ||
10 | VideoFile, | ||
11 | VideoInclude, | ||
12 | VideosCommonQueryAfterSanitize, | ||
13 | VideoStreamingPlaylist | ||
14 | } from '@shared/models' | ||
15 | import { isArray } from '../../../helpers/custom-validators/misc' | ||
16 | import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES, VIDEO_STATES } from '../../../initializers/constants' | ||
17 | import { MServer, MStreamingPlaylistRedundanciesOpt, MVideoFormattable, MVideoFormattableDetails } from '../../../types/models' | ||
18 | import { MVideoFileRedundanciesOpt } from '../../../types/models/video/video-file' | ||
19 | import { sortByResolutionDesc } from './shared' | ||
20 | |||
21 | export type VideoFormattingJSONOptions = { | ||
22 | completeDescription?: boolean | ||
23 | |||
24 | additionalAttributes?: { | ||
25 | state?: boolean | ||
26 | waitTranscoding?: boolean | ||
27 | scheduledUpdate?: boolean | ||
28 | blacklistInfo?: boolean | ||
29 | files?: boolean | ||
30 | blockedOwner?: boolean | ||
31 | } | ||
32 | } | ||
33 | |||
34 | export function guessAdditionalAttributesFromQuery (query: VideosCommonQueryAfterSanitize): VideoFormattingJSONOptions { | ||
35 | if (!query?.include) return {} | ||
36 | |||
37 | return { | ||
38 | additionalAttributes: { | ||
39 | state: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
40 | waitTranscoding: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
41 | scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
42 | blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED), | ||
43 | files: !!(query.include & VideoInclude.FILES), | ||
44 | blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER) | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | |||
49 | // --------------------------------------------------------------------------- | ||
50 | |||
51 | export function videoModelToFormattedJSON (video: MVideoFormattable, options: VideoFormattingJSONOptions = {}): Video { | ||
52 | const span = tracer.startSpan('peertube.VideoModel.toFormattedJSON') | ||
53 | |||
54 | const userHistory = isArray(video.UserVideoHistories) | ||
55 | ? video.UserVideoHistories[0] | ||
56 | : undefined | ||
57 | |||
58 | const videoObject: Video = { | ||
59 | id: video.id, | ||
60 | uuid: video.uuid, | ||
61 | shortUUID: uuidToShort(video.uuid), | ||
62 | |||
63 | url: video.url, | ||
64 | |||
65 | name: video.name, | ||
66 | category: { | ||
67 | id: video.category, | ||
68 | label: getCategoryLabel(video.category) | ||
69 | }, | ||
70 | licence: { | ||
71 | id: video.licence, | ||
72 | label: getLicenceLabel(video.licence) | ||
73 | }, | ||
74 | language: { | ||
75 | id: video.language, | ||
76 | label: getLanguageLabel(video.language) | ||
77 | }, | ||
78 | privacy: { | ||
79 | id: video.privacy, | ||
80 | label: getPrivacyLabel(video.privacy) | ||
81 | }, | ||
82 | nsfw: video.nsfw, | ||
83 | |||
84 | truncatedDescription: video.getTruncatedDescription(), | ||
85 | description: options && options.completeDescription === true | ||
86 | ? video.description | ||
87 | : video.getTruncatedDescription(), | ||
88 | |||
89 | isLocal: video.isOwned(), | ||
90 | duration: video.duration, | ||
91 | |||
92 | views: video.views, | ||
93 | viewers: VideoViewsManager.Instance.getViewers(video), | ||
94 | |||
95 | likes: video.likes, | ||
96 | dislikes: video.dislikes, | ||
97 | thumbnailPath: video.getMiniatureStaticPath(), | ||
98 | previewPath: video.getPreviewStaticPath(), | ||
99 | embedPath: video.getEmbedStaticPath(), | ||
100 | createdAt: video.createdAt, | ||
101 | updatedAt: video.updatedAt, | ||
102 | publishedAt: video.publishedAt, | ||
103 | originallyPublishedAt: video.originallyPublishedAt, | ||
104 | |||
105 | isLive: video.isLive, | ||
106 | |||
107 | account: video.VideoChannel.Account.toFormattedSummaryJSON(), | ||
108 | channel: video.VideoChannel.toFormattedSummaryJSON(), | ||
109 | |||
110 | userHistory: userHistory | ||
111 | ? { currentTime: userHistory.currentTime } | ||
112 | : undefined, | ||
113 | |||
114 | // Can be added by external plugins | ||
115 | pluginData: (video as any).pluginData, | ||
116 | |||
117 | ...buildAdditionalAttributes(video, options) | ||
118 | } | ||
119 | |||
120 | span.end() | ||
121 | |||
122 | return videoObject | ||
123 | } | ||
124 | |||
125 | export function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): VideoDetails { | ||
126 | const span = tracer.startSpan('peertube.VideoModel.toFormattedDetailsJSON') | ||
127 | |||
128 | const videoJSON = video.toFormattedJSON({ | ||
129 | completeDescription: true, | ||
130 | additionalAttributes: { | ||
131 | scheduledUpdate: true, | ||
132 | blacklistInfo: true, | ||
133 | files: true | ||
134 | } | ||
135 | }) as Video & Required<Pick<Video, 'files' | 'streamingPlaylists' | 'scheduledUpdate' | 'blacklisted' | 'blacklistedReason'>> | ||
136 | |||
137 | const tags = video.Tags | ||
138 | ? video.Tags.map(t => t.name) | ||
139 | : [] | ||
140 | |||
141 | const detailsJSON = { | ||
142 | ...videoJSON, | ||
143 | |||
144 | support: video.support, | ||
145 | descriptionPath: video.getDescriptionAPIPath(), | ||
146 | channel: video.VideoChannel.toFormattedJSON(), | ||
147 | account: video.VideoChannel.Account.toFormattedJSON(), | ||
148 | tags, | ||
149 | commentsEnabled: video.commentsEnabled, | ||
150 | downloadEnabled: video.downloadEnabled, | ||
151 | waitTranscoding: video.waitTranscoding, | ||
152 | state: { | ||
153 | id: video.state, | ||
154 | label: getStateLabel(video.state) | ||
155 | }, | ||
156 | |||
157 | trackerUrls: video.getTrackerUrls() | ||
158 | } | ||
159 | |||
160 | span.end() | ||
161 | |||
162 | return detailsJSON | ||
163 | } | ||
164 | |||
165 | export function streamingPlaylistsModelToFormattedJSON ( | ||
166 | video: MVideoFormattable, | ||
167 | playlists: MStreamingPlaylistRedundanciesOpt[] | ||
168 | ): VideoStreamingPlaylist[] { | ||
169 | if (isArray(playlists) === false) return [] | ||
170 | |||
171 | return playlists | ||
172 | .map(playlist => ({ | ||
173 | id: playlist.id, | ||
174 | type: playlist.type, | ||
175 | |||
176 | playlistUrl: playlist.getMasterPlaylistUrl(video), | ||
177 | segmentsSha256Url: playlist.getSha256SegmentsUrl(video), | ||
178 | |||
179 | redundancies: isArray(playlist.RedundancyVideos) | ||
180 | ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) | ||
181 | : [], | ||
182 | |||
183 | files: videoFilesModelToFormattedJSON(video, playlist.VideoFiles) | ||
184 | })) | ||
185 | } | ||
186 | |||
187 | export function videoFilesModelToFormattedJSON ( | ||
188 | video: MVideoFormattable, | ||
189 | videoFiles: MVideoFileRedundanciesOpt[], | ||
190 | options: { | ||
191 | includeMagnet?: boolean // default true | ||
192 | } = {} | ||
193 | ): VideoFile[] { | ||
194 | const { includeMagnet = true } = options | ||
195 | |||
196 | if (isArray(videoFiles) === false) return [] | ||
197 | |||
198 | const trackerUrls = includeMagnet | ||
199 | ? video.getTrackerUrls() | ||
200 | : [] | ||
201 | |||
202 | return videoFiles | ||
203 | .filter(f => !f.isLive()) | ||
204 | .sort(sortByResolutionDesc) | ||
205 | .map(videoFile => { | ||
206 | return { | ||
207 | id: videoFile.id, | ||
208 | |||
209 | resolution: { | ||
210 | id: videoFile.resolution, | ||
211 | label: videoFile.resolution === 0 | ||
212 | ? 'Audio' | ||
213 | : `${videoFile.resolution}p` | ||
214 | }, | ||
215 | |||
216 | magnetUri: includeMagnet && videoFile.hasTorrent() | ||
217 | ? generateMagnetUri(video, videoFile, trackerUrls) | ||
218 | : undefined, | ||
219 | |||
220 | size: videoFile.size, | ||
221 | fps: videoFile.fps, | ||
222 | |||
223 | torrentUrl: videoFile.getTorrentUrl(), | ||
224 | torrentDownloadUrl: videoFile.getTorrentDownloadUrl(), | ||
225 | |||
226 | fileUrl: videoFile.getFileUrl(video), | ||
227 | fileDownloadUrl: videoFile.getFileDownloadUrl(video), | ||
228 | |||
229 | metadataUrl: videoFile.metadataUrl ?? getLocalVideoFileMetadataUrl(video, videoFile) | ||
230 | } | ||
231 | }) | ||
232 | } | ||
233 | |||
234 | // --------------------------------------------------------------------------- | ||
235 | |||
236 | export function getCategoryLabel (id: number) { | ||
237 | return VIDEO_CATEGORIES[id] || 'Unknown' | ||
238 | } | ||
239 | |||
240 | export function getLicenceLabel (id: number) { | ||
241 | return VIDEO_LICENCES[id] || 'Unknown' | ||
242 | } | ||
243 | |||
244 | export function getLanguageLabel (id: string) { | ||
245 | return VIDEO_LANGUAGES[id] || 'Unknown' | ||
246 | } | ||
247 | |||
248 | export function getPrivacyLabel (id: number) { | ||
249 | return VIDEO_PRIVACIES[id] || 'Unknown' | ||
250 | } | ||
251 | |||
252 | export function getStateLabel (id: number) { | ||
253 | return VIDEO_STATES[id] || 'Unknown' | ||
254 | } | ||
255 | |||
256 | // --------------------------------------------------------------------------- | ||
257 | // Private | ||
258 | // --------------------------------------------------------------------------- | ||
259 | |||
260 | function buildAdditionalAttributes (video: MVideoFormattable, options: VideoFormattingJSONOptions) { | ||
261 | const add = options.additionalAttributes | ||
262 | |||
263 | const result: Partial<VideoAdditionalAttributes> = {} | ||
264 | |||
265 | if (add?.state === true) { | ||
266 | result.state = { | ||
267 | id: video.state, | ||
268 | label: getStateLabel(video.state) | ||
269 | } | ||
270 | } | ||
271 | |||
272 | if (add?.waitTranscoding === true) { | ||
273 | result.waitTranscoding = video.waitTranscoding | ||
274 | } | ||
275 | |||
276 | if (add?.scheduledUpdate === true && video.ScheduleVideoUpdate) { | ||
277 | result.scheduledUpdate = { | ||
278 | updateAt: video.ScheduleVideoUpdate.updateAt, | ||
279 | privacy: video.ScheduleVideoUpdate.privacy || undefined | ||
280 | } | ||
281 | } | ||
282 | |||
283 | if (add?.blacklistInfo === true) { | ||
284 | result.blacklisted = !!video.VideoBlacklist | ||
285 | result.blacklistedReason = | ||
286 | video.VideoBlacklist | ||
287 | ? video.VideoBlacklist.reason | ||
288 | : null | ||
289 | } | ||
290 | |||
291 | if (add?.blockedOwner === true) { | ||
292 | result.blockedOwner = video.VideoChannel.Account.isBlocked() | ||
293 | |||
294 | const server = video.VideoChannel.Account.Actor.Server as MServer | ||
295 | result.blockedServer = !!(server?.isBlocked()) | ||
296 | } | ||
297 | |||
298 | if (add?.files === true) { | ||
299 | result.streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) | ||
300 | result.files = videoFilesModelToFormattedJSON(video, video.VideoFiles) | ||
301 | } | ||
302 | |||
303 | return result | ||
304 | } | ||
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts deleted file mode 100644 index f2001e432..000000000 --- a/server/models/video/formatter/video-format-utils.ts +++ /dev/null | |||
@@ -1,543 +0,0 @@ | |||
1 | import { generateMagnetUri } from '@server/helpers/webtorrent' | ||
2 | import { getActivityStreamDuration } from '@server/lib/activitypub/activity' | ||
3 | import { tracer } from '@server/lib/opentelemetry/tracing' | ||
4 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls' | ||
5 | import { VideoViewsManager } from '@server/lib/views/video-views-manager' | ||
6 | import { uuidToShort } from '@shared/extra-utils' | ||
7 | import { | ||
8 | ActivityTagObject, | ||
9 | ActivityUrlObject, | ||
10 | Video, | ||
11 | VideoDetails, | ||
12 | VideoFile, | ||
13 | VideoInclude, | ||
14 | VideoObject, | ||
15 | VideosCommonQueryAfterSanitize, | ||
16 | VideoStreamingPlaylist | ||
17 | } from '@shared/models' | ||
18 | import { isArray } from '../../../helpers/custom-validators/misc' | ||
19 | import { | ||
20 | MIMETYPES, | ||
21 | VIDEO_CATEGORIES, | ||
22 | VIDEO_LANGUAGES, | ||
23 | VIDEO_LICENCES, | ||
24 | VIDEO_PRIVACIES, | ||
25 | VIDEO_STATES, | ||
26 | WEBSERVER | ||
27 | } from '../../../initializers/constants' | ||
28 | import { | ||
29 | getLocalVideoCommentsActivityPubUrl, | ||
30 | getLocalVideoDislikesActivityPubUrl, | ||
31 | getLocalVideoLikesActivityPubUrl, | ||
32 | getLocalVideoSharesActivityPubUrl | ||
33 | } from '../../../lib/activitypub/url' | ||
34 | import { | ||
35 | MServer, | ||
36 | MStreamingPlaylistRedundanciesOpt, | ||
37 | MUserId, | ||
38 | MVideo, | ||
39 | MVideoAP, | ||
40 | MVideoFile, | ||
41 | MVideoFormattable, | ||
42 | MVideoFormattableDetails | ||
43 | } from '../../../types/models' | ||
44 | import { MVideoFileRedundanciesOpt } from '../../../types/models/video/video-file' | ||
45 | import { VideoCaptionModel } from '../video-caption' | ||
46 | |||
47 | export type VideoFormattingJSONOptions = { | ||
48 | completeDescription?: boolean | ||
49 | |||
50 | additionalAttributes?: { | ||
51 | state?: boolean | ||
52 | waitTranscoding?: boolean | ||
53 | scheduledUpdate?: boolean | ||
54 | blacklistInfo?: boolean | ||
55 | files?: boolean | ||
56 | blockedOwner?: boolean | ||
57 | } | ||
58 | } | ||
59 | |||
60 | function guessAdditionalAttributesFromQuery (query: VideosCommonQueryAfterSanitize): VideoFormattingJSONOptions { | ||
61 | if (!query?.include) return {} | ||
62 | |||
63 | return { | ||
64 | additionalAttributes: { | ||
65 | state: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
66 | waitTranscoding: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
67 | scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
68 | blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED), | ||
69 | files: !!(query.include & VideoInclude.FILES), | ||
70 | blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER) | ||
71 | } | ||
72 | } | ||
73 | } | ||
74 | |||
75 | function videoModelToFormattedJSON (video: MVideoFormattable, options: VideoFormattingJSONOptions = {}): Video { | ||
76 | const span = tracer.startSpan('peertube.VideoModel.toFormattedJSON') | ||
77 | |||
78 | const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined | ||
79 | |||
80 | const videoObject: Video = { | ||
81 | id: video.id, | ||
82 | uuid: video.uuid, | ||
83 | shortUUID: uuidToShort(video.uuid), | ||
84 | |||
85 | url: video.url, | ||
86 | |||
87 | name: video.name, | ||
88 | category: { | ||
89 | id: video.category, | ||
90 | label: getCategoryLabel(video.category) | ||
91 | }, | ||
92 | licence: { | ||
93 | id: video.licence, | ||
94 | label: getLicenceLabel(video.licence) | ||
95 | }, | ||
96 | language: { | ||
97 | id: video.language, | ||
98 | label: getLanguageLabel(video.language) | ||
99 | }, | ||
100 | privacy: { | ||
101 | id: video.privacy, | ||
102 | label: getPrivacyLabel(video.privacy) | ||
103 | }, | ||
104 | nsfw: video.nsfw, | ||
105 | |||
106 | truncatedDescription: video.getTruncatedDescription(), | ||
107 | description: options && options.completeDescription === true | ||
108 | ? video.description | ||
109 | : video.getTruncatedDescription(), | ||
110 | |||
111 | isLocal: video.isOwned(), | ||
112 | duration: video.duration, | ||
113 | |||
114 | views: video.views, | ||
115 | viewers: VideoViewsManager.Instance.getViewers(video), | ||
116 | |||
117 | likes: video.likes, | ||
118 | dislikes: video.dislikes, | ||
119 | thumbnailPath: video.getMiniatureStaticPath(), | ||
120 | previewPath: video.getPreviewStaticPath(), | ||
121 | embedPath: video.getEmbedStaticPath(), | ||
122 | createdAt: video.createdAt, | ||
123 | updatedAt: video.updatedAt, | ||
124 | publishedAt: video.publishedAt, | ||
125 | originallyPublishedAt: video.originallyPublishedAt, | ||
126 | |||
127 | isLive: video.isLive, | ||
128 | |||
129 | account: video.VideoChannel.Account.toFormattedSummaryJSON(), | ||
130 | channel: video.VideoChannel.toFormattedSummaryJSON(), | ||
131 | |||
132 | userHistory: userHistory | ||
133 | ? { currentTime: userHistory.currentTime } | ||
134 | : undefined, | ||
135 | |||
136 | // Can be added by external plugins | ||
137 | pluginData: (video as any).pluginData | ||
138 | } | ||
139 | |||
140 | const add = options.additionalAttributes | ||
141 | if (add?.state === true) { | ||
142 | videoObject.state = { | ||
143 | id: video.state, | ||
144 | label: getStateLabel(video.state) | ||
145 | } | ||
146 | } | ||
147 | |||
148 | if (add?.waitTranscoding === true) { | ||
149 | videoObject.waitTranscoding = video.waitTranscoding | ||
150 | } | ||
151 | |||
152 | if (add?.scheduledUpdate === true && video.ScheduleVideoUpdate) { | ||
153 | videoObject.scheduledUpdate = { | ||
154 | updateAt: video.ScheduleVideoUpdate.updateAt, | ||
155 | privacy: video.ScheduleVideoUpdate.privacy || undefined | ||
156 | } | ||
157 | } | ||
158 | |||
159 | if (add?.blacklistInfo === true) { | ||
160 | videoObject.blacklisted = !!video.VideoBlacklist | ||
161 | videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null | ||
162 | } | ||
163 | |||
164 | if (add?.blockedOwner === true) { | ||
165 | videoObject.blockedOwner = video.VideoChannel.Account.isBlocked() | ||
166 | |||
167 | const server = video.VideoChannel.Account.Actor.Server as MServer | ||
168 | videoObject.blockedServer = !!(server?.isBlocked()) | ||
169 | } | ||
170 | |||
171 | if (add?.files === true) { | ||
172 | videoObject.streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) | ||
173 | videoObject.files = videoFilesModelToFormattedJSON(video, video.VideoFiles) | ||
174 | } | ||
175 | |||
176 | span.end() | ||
177 | |||
178 | return videoObject | ||
179 | } | ||
180 | |||
181 | function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): VideoDetails { | ||
182 | const span = tracer.startSpan('peertube.VideoModel.toFormattedDetailsJSON') | ||
183 | |||
184 | const videoJSON = video.toFormattedJSON({ | ||
185 | completeDescription: true, | ||
186 | additionalAttributes: { | ||
187 | scheduledUpdate: true, | ||
188 | blacklistInfo: true, | ||
189 | files: true | ||
190 | } | ||
191 | }) as Video & Required<Pick<Video, 'files' | 'streamingPlaylists'>> | ||
192 | |||
193 | const tags = video.Tags ? video.Tags.map(t => t.name) : [] | ||
194 | |||
195 | const detailsJSON = { | ||
196 | support: video.support, | ||
197 | descriptionPath: video.getDescriptionAPIPath(), | ||
198 | channel: video.VideoChannel.toFormattedJSON(), | ||
199 | account: video.VideoChannel.Account.toFormattedJSON(), | ||
200 | tags, | ||
201 | commentsEnabled: video.commentsEnabled, | ||
202 | downloadEnabled: video.downloadEnabled, | ||
203 | waitTranscoding: video.waitTranscoding, | ||
204 | state: { | ||
205 | id: video.state, | ||
206 | label: getStateLabel(video.state) | ||
207 | }, | ||
208 | |||
209 | trackerUrls: video.getTrackerUrls() | ||
210 | } | ||
211 | |||
212 | span.end() | ||
213 | |||
214 | return Object.assign(videoJSON, detailsJSON) | ||
215 | } | ||
216 | |||
217 | function streamingPlaylistsModelToFormattedJSON ( | ||
218 | video: MVideoFormattable, | ||
219 | playlists: MStreamingPlaylistRedundanciesOpt[] | ||
220 | ): VideoStreamingPlaylist[] { | ||
221 | if (isArray(playlists) === false) return [] | ||
222 | |||
223 | return playlists | ||
224 | .map(playlist => { | ||
225 | const redundancies = isArray(playlist.RedundancyVideos) | ||
226 | ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) | ||
227 | : [] | ||
228 | |||
229 | const files = videoFilesModelToFormattedJSON(video, playlist.VideoFiles) | ||
230 | |||
231 | return { | ||
232 | id: playlist.id, | ||
233 | type: playlist.type, | ||
234 | playlistUrl: playlist.getMasterPlaylistUrl(video), | ||
235 | segmentsSha256Url: playlist.getSha256SegmentsUrl(video), | ||
236 | redundancies, | ||
237 | files | ||
238 | } | ||
239 | }) | ||
240 | } | ||
241 | |||
242 | function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) { | ||
243 | if (fileA.resolution < fileB.resolution) return 1 | ||
244 | if (fileA.resolution === fileB.resolution) return 0 | ||
245 | return -1 | ||
246 | } | ||
247 | |||
248 | function videoFilesModelToFormattedJSON ( | ||
249 | video: MVideoFormattable, | ||
250 | videoFiles: MVideoFileRedundanciesOpt[], | ||
251 | options: { | ||
252 | includeMagnet?: boolean // default true | ||
253 | } = {} | ||
254 | ): VideoFile[] { | ||
255 | const { includeMagnet = true } = options | ||
256 | |||
257 | const trackerUrls = includeMagnet | ||
258 | ? video.getTrackerUrls() | ||
259 | : [] | ||
260 | |||
261 | return (videoFiles || []) | ||
262 | .filter(f => !f.isLive()) | ||
263 | .sort(sortByResolutionDesc) | ||
264 | .map(videoFile => { | ||
265 | return { | ||
266 | id: videoFile.id, | ||
267 | |||
268 | resolution: { | ||
269 | id: videoFile.resolution, | ||
270 | label: videoFile.resolution === 0 ? 'Audio' : `${videoFile.resolution}p` | ||
271 | }, | ||
272 | |||
273 | magnetUri: includeMagnet && videoFile.hasTorrent() | ||
274 | ? generateMagnetUri(video, videoFile, trackerUrls) | ||
275 | : undefined, | ||
276 | |||
277 | size: videoFile.size, | ||
278 | fps: videoFile.fps, | ||
279 | |||
280 | torrentUrl: videoFile.getTorrentUrl(), | ||
281 | torrentDownloadUrl: videoFile.getTorrentDownloadUrl(), | ||
282 | |||
283 | fileUrl: videoFile.getFileUrl(video), | ||
284 | fileDownloadUrl: videoFile.getFileDownloadUrl(video), | ||
285 | |||
286 | metadataUrl: videoFile.metadataUrl ?? getLocalVideoFileMetadataUrl(video, videoFile) | ||
287 | } as VideoFile | ||
288 | }) | ||
289 | } | ||
290 | |||
291 | function addVideoFilesInAPAcc (options: { | ||
292 | acc: ActivityUrlObject[] | ActivityTagObject[] | ||
293 | video: MVideo | ||
294 | files: MVideoFile[] | ||
295 | user?: MUserId | ||
296 | }) { | ||
297 | const { acc, video, files } = options | ||
298 | |||
299 | const trackerUrls = video.getTrackerUrls() | ||
300 | |||
301 | const sortedFiles = (files || []) | ||
302 | .filter(f => !f.isLive()) | ||
303 | .sort(sortByResolutionDesc) | ||
304 | |||
305 | for (const file of sortedFiles) { | ||
306 | acc.push({ | ||
307 | type: 'Link', | ||
308 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any, | ||
309 | href: file.getFileUrl(video), | ||
310 | height: file.resolution, | ||
311 | size: file.size, | ||
312 | fps: file.fps | ||
313 | }) | ||
314 | |||
315 | acc.push({ | ||
316 | type: 'Link', | ||
317 | rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ], | ||
318 | mediaType: 'application/json' as 'application/json', | ||
319 | href: getLocalVideoFileMetadataUrl(video, file), | ||
320 | height: file.resolution, | ||
321 | fps: file.fps | ||
322 | }) | ||
323 | |||
324 | if (file.hasTorrent()) { | ||
325 | acc.push({ | ||
326 | type: 'Link', | ||
327 | mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
328 | href: file.getTorrentUrl(), | ||
329 | height: file.resolution | ||
330 | }) | ||
331 | |||
332 | acc.push({ | ||
333 | type: 'Link', | ||
334 | mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
335 | href: generateMagnetUri(video, file, trackerUrls), | ||
336 | height: file.resolution | ||
337 | }) | ||
338 | } | ||
339 | } | ||
340 | } | ||
341 | |||
342 | function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | ||
343 | if (!video.Tags) video.Tags = [] | ||
344 | |||
345 | const tag = video.Tags.map(t => ({ | ||
346 | type: 'Hashtag' as 'Hashtag', | ||
347 | name: t.name | ||
348 | })) | ||
349 | |||
350 | let language | ||
351 | if (video.language) { | ||
352 | language = { | ||
353 | identifier: video.language, | ||
354 | name: getLanguageLabel(video.language) | ||
355 | } | ||
356 | } | ||
357 | |||
358 | let category | ||
359 | if (video.category) { | ||
360 | category = { | ||
361 | identifier: video.category + '', | ||
362 | name: getCategoryLabel(video.category) | ||
363 | } | ||
364 | } | ||
365 | |||
366 | let licence | ||
367 | if (video.licence) { | ||
368 | licence = { | ||
369 | identifier: video.licence + '', | ||
370 | name: getLicenceLabel(video.licence) | ||
371 | } | ||
372 | } | ||
373 | |||
374 | const url: ActivityUrlObject[] = [ | ||
375 | // HTML url should be the first element in the array so Mastodon correctly displays the embed | ||
376 | { | ||
377 | type: 'Link', | ||
378 | mediaType: 'text/html', | ||
379 | href: WEBSERVER.URL + '/videos/watch/' + video.uuid | ||
380 | } | ||
381 | ] | ||
382 | |||
383 | addVideoFilesInAPAcc({ acc: url, video, files: video.VideoFiles || [] }) | ||
384 | |||
385 | for (const playlist of (video.VideoStreamingPlaylists || [])) { | ||
386 | const tag = playlist.p2pMediaLoaderInfohashes | ||
387 | .map(i => ({ type: 'Infohash' as 'Infohash', name: i })) as ActivityTagObject[] | ||
388 | tag.push({ | ||
389 | type: 'Link', | ||
390 | name: 'sha256', | ||
391 | mediaType: 'application/json' as 'application/json', | ||
392 | href: playlist.getSha256SegmentsUrl(video) | ||
393 | }) | ||
394 | |||
395 | addVideoFilesInAPAcc({ acc: tag, video, files: playlist.VideoFiles || [] }) | ||
396 | |||
397 | url.push({ | ||
398 | type: 'Link', | ||
399 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', | ||
400 | href: playlist.getMasterPlaylistUrl(video), | ||
401 | tag | ||
402 | }) | ||
403 | } | ||
404 | |||
405 | for (const trackerUrl of video.getTrackerUrls()) { | ||
406 | const rel2 = trackerUrl.startsWith('http') | ||
407 | ? 'http' | ||
408 | : 'websocket' | ||
409 | |||
410 | url.push({ | ||
411 | type: 'Link', | ||
412 | name: `tracker-${rel2}`, | ||
413 | rel: [ 'tracker', rel2 ], | ||
414 | href: trackerUrl | ||
415 | }) | ||
416 | } | ||
417 | |||
418 | const subtitleLanguage = [] | ||
419 | for (const caption of video.VideoCaptions) { | ||
420 | subtitleLanguage.push({ | ||
421 | identifier: caption.language, | ||
422 | name: VideoCaptionModel.getLanguageLabel(caption.language), | ||
423 | url: caption.getFileUrl(video) | ||
424 | }) | ||
425 | } | ||
426 | |||
427 | const icons = [ video.getMiniature(), video.getPreview() ] | ||
428 | |||
429 | return { | ||
430 | type: 'Video' as 'Video', | ||
431 | id: video.url, | ||
432 | name: video.name, | ||
433 | duration: getActivityStreamDuration(video.duration), | ||
434 | uuid: video.uuid, | ||
435 | tag, | ||
436 | category, | ||
437 | licence, | ||
438 | language, | ||
439 | views: video.views, | ||
440 | sensitive: video.nsfw, | ||
441 | waitTranscoding: video.waitTranscoding, | ||
442 | |||
443 | state: video.state, | ||
444 | commentsEnabled: video.commentsEnabled, | ||
445 | downloadEnabled: video.downloadEnabled, | ||
446 | published: video.publishedAt.toISOString(), | ||
447 | |||
448 | originallyPublishedAt: video.originallyPublishedAt | ||
449 | ? video.originallyPublishedAt.toISOString() | ||
450 | : null, | ||
451 | |||
452 | updated: video.updatedAt.toISOString(), | ||
453 | |||
454 | mediaType: 'text/markdown', | ||
455 | content: video.description, | ||
456 | support: video.support, | ||
457 | |||
458 | subtitleLanguage, | ||
459 | |||
460 | icon: icons.map(i => ({ | ||
461 | type: 'Image', | ||
462 | url: i.getOriginFileUrl(video), | ||
463 | mediaType: 'image/jpeg', | ||
464 | width: i.width, | ||
465 | height: i.height | ||
466 | })), | ||
467 | |||
468 | url, | ||
469 | |||
470 | likes: getLocalVideoLikesActivityPubUrl(video), | ||
471 | dislikes: getLocalVideoDislikesActivityPubUrl(video), | ||
472 | shares: getLocalVideoSharesActivityPubUrl(video), | ||
473 | comments: getLocalVideoCommentsActivityPubUrl(video), | ||
474 | |||
475 | attributedTo: [ | ||
476 | { | ||
477 | type: 'Person', | ||
478 | id: video.VideoChannel.Account.Actor.url | ||
479 | }, | ||
480 | { | ||
481 | type: 'Group', | ||
482 | id: video.VideoChannel.Actor.url | ||
483 | } | ||
484 | ], | ||
485 | |||
486 | ...buildLiveAPAttributes(video) | ||
487 | } | ||
488 | } | ||
489 | |||
490 | function getCategoryLabel (id: number) { | ||
491 | return VIDEO_CATEGORIES[id] || 'Unknown' | ||
492 | } | ||
493 | |||
494 | function getLicenceLabel (id: number) { | ||
495 | return VIDEO_LICENCES[id] || 'Unknown' | ||
496 | } | ||
497 | |||
498 | function getLanguageLabel (id: string) { | ||
499 | return VIDEO_LANGUAGES[id] || 'Unknown' | ||
500 | } | ||
501 | |||
502 | function getPrivacyLabel (id: number) { | ||
503 | return VIDEO_PRIVACIES[id] || 'Unknown' | ||
504 | } | ||
505 | |||
506 | function getStateLabel (id: number) { | ||
507 | return VIDEO_STATES[id] || 'Unknown' | ||
508 | } | ||
509 | |||
510 | export { | ||
511 | videoModelToFormattedJSON, | ||
512 | videoModelToFormattedDetailsJSON, | ||
513 | videoFilesModelToFormattedJSON, | ||
514 | videoModelToActivityPubObject, | ||
515 | |||
516 | guessAdditionalAttributesFromQuery, | ||
517 | |||
518 | getCategoryLabel, | ||
519 | getLicenceLabel, | ||
520 | getLanguageLabel, | ||
521 | getPrivacyLabel, | ||
522 | getStateLabel | ||
523 | } | ||
524 | |||
525 | // --------------------------------------------------------------------------- | ||
526 | |||
527 | function buildLiveAPAttributes (video: MVideoAP) { | ||
528 | if (!video.isLive) { | ||
529 | return { | ||
530 | isLiveBroadcast: false, | ||
531 | liveSaveReplay: null, | ||
532 | permanentLive: null, | ||
533 | latencyMode: null | ||
534 | } | ||
535 | } | ||
536 | |||
537 | return { | ||
538 | isLiveBroadcast: true, | ||
539 | liveSaveReplay: video.VideoLive.saveReplay, | ||
540 | permanentLive: video.VideoLive.permanentLive, | ||
541 | latencyMode: video.VideoLive.latencyMode | ||
542 | } | ||
543 | } | ||
diff --git a/server/models/video/sql/video/shared/abstract-video-query-builder.ts b/server/models/video/sql/video/shared/abstract-video-query-builder.ts index cbd57ad8c..56a00aa0c 100644 --- a/server/models/video/sql/video/shared/abstract-video-query-builder.ts +++ b/server/models/video/sql/video/shared/abstract-video-query-builder.ts | |||
@@ -111,7 +111,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
111 | } | 111 | } |
112 | } | 112 | } |
113 | 113 | ||
114 | protected includeWebtorrentFiles () { | 114 | protected includeWebVideoFiles () { |
115 | this.addJoin('LEFT JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"') | 115 | this.addJoin('LEFT JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"') |
116 | 116 | ||
117 | this.attributes = { | 117 | this.attributes = { |
@@ -263,7 +263,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
263 | } | 263 | } |
264 | } | 264 | } |
265 | 265 | ||
266 | protected includeWebTorrentRedundancies () { | 266 | protected includeWebVideoRedundancies () { |
267 | this.addJoin( | 267 | this.addJoin( |
268 | 'LEFT OUTER JOIN "videoRedundancy" AS "VideoFiles->RedundancyVideos" ON ' + | 268 | 'LEFT OUTER JOIN "videoRedundancy" AS "VideoFiles->RedundancyVideos" ON ' + |
269 | '"VideoFiles"."id" = "VideoFiles->RedundancyVideos"."videoFileId"' | 269 | '"VideoFiles"."id" = "VideoFiles->RedundancyVideos"."videoFileId"' |
diff --git a/server/models/video/sql/video/shared/video-file-query-builder.ts b/server/models/video/sql/video/shared/video-file-query-builder.ts index cc53a4860..196b72b43 100644 --- a/server/models/video/sql/video/shared/video-file-query-builder.ts +++ b/server/models/video/sql/video/shared/video-file-query-builder.ts | |||
@@ -14,7 +14,7 @@ export type FileQueryOptions = { | |||
14 | 14 | ||
15 | /** | 15 | /** |
16 | * | 16 | * |
17 | * Fetch files (webtorrent and streaming playlist) according to a video | 17 | * Fetch files (web videos and streaming playlist) according to a video |
18 | * | 18 | * |
19 | */ | 19 | */ |
20 | 20 | ||
@@ -25,8 +25,8 @@ export class VideoFileQueryBuilder extends AbstractVideoQueryBuilder { | |||
25 | super(sequelize, 'get') | 25 | super(sequelize, 'get') |
26 | } | 26 | } |
27 | 27 | ||
28 | queryWebTorrentVideos (options: FileQueryOptions) { | 28 | queryWebVideos (options: FileQueryOptions) { |
29 | this.buildWebtorrentFilesQuery(options) | 29 | this.buildWebVideoFilesQuery(options) |
30 | 30 | ||
31 | return this.runQuery(options) | 31 | return this.runQuery(options) |
32 | } | 32 | } |
@@ -37,15 +37,15 @@ export class VideoFileQueryBuilder extends AbstractVideoQueryBuilder { | |||
37 | return this.runQuery(options) | 37 | return this.runQuery(options) |
38 | } | 38 | } |
39 | 39 | ||
40 | private buildWebtorrentFilesQuery (options: FileQueryOptions) { | 40 | private buildWebVideoFilesQuery (options: FileQueryOptions) { |
41 | this.attributes = { | 41 | this.attributes = { |
42 | '"video"."id"': '' | 42 | '"video"."id"': '' |
43 | } | 43 | } |
44 | 44 | ||
45 | this.includeWebtorrentFiles() | 45 | this.includeWebVideoFiles() |
46 | 46 | ||
47 | if (options.includeRedundancy) { | 47 | if (options.includeRedundancy) { |
48 | this.includeWebTorrentRedundancies() | 48 | this.includeWebVideoRedundancies() |
49 | } | 49 | } |
50 | 50 | ||
51 | this.whereId(options) | 51 | this.whereId(options) |
diff --git a/server/models/video/sql/video/shared/video-model-builder.ts b/server/models/video/sql/video/shared/video-model-builder.ts index 0a2beb7db..740aa842f 100644 --- a/server/models/video/sql/video/shared/video-model-builder.ts +++ b/server/models/video/sql/video/shared/video-model-builder.ts | |||
@@ -60,10 +60,10 @@ export class VideoModelBuilder { | |||
60 | buildVideosFromRows (options: { | 60 | buildVideosFromRows (options: { |
61 | rows: SQLRow[] | 61 | rows: SQLRow[] |
62 | include?: VideoInclude | 62 | include?: VideoInclude |
63 | rowsWebTorrentFiles?: SQLRow[] | 63 | rowsWebVideoFiles?: SQLRow[] |
64 | rowsStreamingPlaylist?: SQLRow[] | 64 | rowsStreamingPlaylist?: SQLRow[] |
65 | }) { | 65 | }) { |
66 | const { rows, rowsWebTorrentFiles, rowsStreamingPlaylist, include } = options | 66 | const { rows, rowsWebVideoFiles, rowsStreamingPlaylist, include } = options |
67 | 67 | ||
68 | this.reinit() | 68 | this.reinit() |
69 | 69 | ||
@@ -85,8 +85,8 @@ export class VideoModelBuilder { | |||
85 | this.addActorAvatar(row, 'VideoChannel.Account.Actor', accountActor) | 85 | this.addActorAvatar(row, 'VideoChannel.Account.Actor', accountActor) |
86 | } | 86 | } |
87 | 87 | ||
88 | if (!rowsWebTorrentFiles) { | 88 | if (!rowsWebVideoFiles) { |
89 | this.addWebTorrentFile(row, videoModel) | 89 | this.addWebVideoFile(row, videoModel) |
90 | } | 90 | } |
91 | 91 | ||
92 | if (!rowsStreamingPlaylist) { | 92 | if (!rowsStreamingPlaylist) { |
@@ -112,7 +112,7 @@ export class VideoModelBuilder { | |||
112 | } | 112 | } |
113 | } | 113 | } |
114 | 114 | ||
115 | this.grabSeparateWebTorrentFiles(rowsWebTorrentFiles) | 115 | this.grabSeparateWebVideoFiles(rowsWebVideoFiles) |
116 | this.grabSeparateStreamingPlaylistFiles(rowsStreamingPlaylist) | 116 | this.grabSeparateStreamingPlaylistFiles(rowsStreamingPlaylist) |
117 | 117 | ||
118 | return this.videos | 118 | return this.videos |
@@ -140,15 +140,15 @@ export class VideoModelBuilder { | |||
140 | this.videos = [] | 140 | this.videos = [] |
141 | } | 141 | } |
142 | 142 | ||
143 | private grabSeparateWebTorrentFiles (rowsWebTorrentFiles?: SQLRow[]) { | 143 | private grabSeparateWebVideoFiles (rowsWebVideoFiles?: SQLRow[]) { |
144 | if (!rowsWebTorrentFiles) return | 144 | if (!rowsWebVideoFiles) return |
145 | 145 | ||
146 | for (const row of rowsWebTorrentFiles) { | 146 | for (const row of rowsWebVideoFiles) { |
147 | const id = row['VideoFiles.id'] | 147 | const id = row['VideoFiles.id'] |
148 | if (!id) continue | 148 | if (!id) continue |
149 | 149 | ||
150 | const videoModel = this.videosMemo[row.id] | 150 | const videoModel = this.videosMemo[row.id] |
151 | this.addWebTorrentFile(row, videoModel) | 151 | this.addWebVideoFile(row, videoModel) |
152 | this.addRedundancy(row, 'VideoFiles', this.videoFileMemo[id]) | 152 | this.addRedundancy(row, 'VideoFiles', this.videoFileMemo[id]) |
153 | } | 153 | } |
154 | } | 154 | } |
@@ -258,7 +258,7 @@ export class VideoModelBuilder { | |||
258 | this.thumbnailsDone.add(id) | 258 | this.thumbnailsDone.add(id) |
259 | } | 259 | } |
260 | 260 | ||
261 | private addWebTorrentFile (row: SQLRow, videoModel: VideoModel) { | 261 | private addWebVideoFile (row: SQLRow, videoModel: VideoModel) { |
262 | const id = row['VideoFiles.id'] | 262 | const id = row['VideoFiles.id'] |
263 | if (!id || this.videoFileMemo[id]) return | 263 | if (!id || this.videoFileMemo[id]) return |
264 | 264 | ||
diff --git a/server/models/video/sql/video/shared/video-table-attributes.ts b/server/models/video/sql/video/shared/video-table-attributes.ts index 34967cd20..e0fa9d7c1 100644 --- a/server/models/video/sql/video/shared/video-table-attributes.ts +++ b/server/models/video/sql/video/shared/video-table-attributes.ts | |||
@@ -60,6 +60,7 @@ export class VideoTableAttributes { | |||
60 | 'height', | 60 | 'height', |
61 | 'width', | 61 | 'width', |
62 | 'fileUrl', | 62 | 'fileUrl', |
63 | 'onDisk', | ||
63 | 'automaticallyGenerated', | 64 | 'automaticallyGenerated', |
64 | 'videoId', | 65 | 'videoId', |
65 | 'videoPlaylistId', | 66 | 'videoPlaylistId', |
diff --git a/server/models/video/sql/video/video-model-get-query-builder.ts b/server/models/video/sql/video/video-model-get-query-builder.ts index 8e90ff641..3f43d4d92 100644 --- a/server/models/video/sql/video/video-model-get-query-builder.ts +++ b/server/models/video/sql/video/video-model-get-query-builder.ts | |||
@@ -35,7 +35,7 @@ export type BuildVideoGetQueryOptions = { | |||
35 | 35 | ||
36 | export class VideoModelGetQueryBuilder { | 36 | export class VideoModelGetQueryBuilder { |
37 | videoQueryBuilder: VideosModelGetQuerySubBuilder | 37 | videoQueryBuilder: VideosModelGetQuerySubBuilder |
38 | webtorrentFilesQueryBuilder: VideoFileQueryBuilder | 38 | webVideoFilesQueryBuilder: VideoFileQueryBuilder |
39 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder | 39 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder |
40 | 40 | ||
41 | private readonly videoModelBuilder: VideoModelBuilder | 41 | private readonly videoModelBuilder: VideoModelBuilder |
@@ -44,7 +44,7 @@ export class VideoModelGetQueryBuilder { | |||
44 | 44 | ||
45 | constructor (protected readonly sequelize: Sequelize) { | 45 | constructor (protected readonly sequelize: Sequelize) { |
46 | this.videoQueryBuilder = new VideosModelGetQuerySubBuilder(sequelize) | 46 | this.videoQueryBuilder = new VideosModelGetQuerySubBuilder(sequelize) |
47 | this.webtorrentFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | 47 | this.webVideoFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) |
48 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | 48 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) |
49 | 49 | ||
50 | this.videoModelBuilder = new VideoModelBuilder('get', new VideoTableAttributes('get')) | 50 | this.videoModelBuilder = new VideoModelBuilder('get', new VideoTableAttributes('get')) |
@@ -57,11 +57,11 @@ export class VideoModelGetQueryBuilder { | |||
57 | includeRedundancy: this.shouldIncludeRedundancies(options) | 57 | includeRedundancy: this.shouldIncludeRedundancies(options) |
58 | } | 58 | } |
59 | 59 | ||
60 | const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([ | 60 | const [ videoRows, webVideoFilesRows, streamingPlaylistFilesRows ] = await Promise.all([ |
61 | this.videoQueryBuilder.queryVideos(options), | 61 | this.videoQueryBuilder.queryVideos(options), |
62 | 62 | ||
63 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) | 63 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) |
64 | ? this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(fileQueryOptions) | 64 | ? this.webVideoFilesQueryBuilder.queryWebVideos(fileQueryOptions) |
65 | : Promise.resolve(undefined), | 65 | : Promise.resolve(undefined), |
66 | 66 | ||
67 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) | 67 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) |
@@ -71,7 +71,7 @@ export class VideoModelGetQueryBuilder { | |||
71 | 71 | ||
72 | const videos = this.videoModelBuilder.buildVideosFromRows({ | 72 | const videos = this.videoModelBuilder.buildVideosFromRows({ |
73 | rows: videoRows, | 73 | rows: videoRows, |
74 | rowsWebTorrentFiles: webtorrentFilesRows, | 74 | rowsWebVideoFiles: webVideoFilesRows, |
75 | rowsStreamingPlaylist: streamingPlaylistFilesRows | 75 | rowsStreamingPlaylist: streamingPlaylistFilesRows |
76 | }) | 76 | }) |
77 | 77 | ||
@@ -92,7 +92,7 @@ export class VideoModelGetQueryBuilder { | |||
92 | export class VideosModelGetQuerySubBuilder extends AbstractVideoQueryBuilder { | 92 | export class VideosModelGetQuerySubBuilder extends AbstractVideoQueryBuilder { |
93 | protected attributes: { [key: string]: string } | 93 | protected attributes: { [key: string]: string } |
94 | 94 | ||
95 | protected webtorrentFilesQuery: string | 95 | protected webVideoFilesQuery: string |
96 | protected streamingPlaylistFilesQuery: string | 96 | protected streamingPlaylistFilesQuery: string |
97 | 97 | ||
98 | private static readonly trackersInclude = new Set<GetType>([ 'api' ]) | 98 | private static readonly trackersInclude = new Set<GetType>([ 'api' ]) |
diff --git a/server/models/video/sql/video/videos-id-list-query-builder.ts b/server/models/video/sql/video/videos-id-list-query-builder.ts index cba77c1d1..7f2376102 100644 --- a/server/models/video/sql/video/videos-id-list-query-builder.ts +++ b/server/models/video/sql/video/videos-id-list-query-builder.ts | |||
@@ -48,7 +48,9 @@ export type BuildVideosListQueryOptions = { | |||
48 | 48 | ||
49 | hasFiles?: boolean | 49 | hasFiles?: boolean |
50 | hasHLSFiles?: boolean | 50 | hasHLSFiles?: boolean |
51 | hasWebtorrentFiles?: boolean | 51 | |
52 | hasWebVideoFiles?: boolean | ||
53 | hasWebtorrentFiles?: boolean // TODO: Remove in v7 | ||
52 | 54 | ||
53 | accountId?: number | 55 | accountId?: number |
54 | videoChannelId?: number | 56 | videoChannelId?: number |
@@ -175,7 +177,9 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { | |||
175 | } | 177 | } |
176 | 178 | ||
177 | if (exists(options.hasWebtorrentFiles)) { | 179 | if (exists(options.hasWebtorrentFiles)) { |
178 | this.whereWebTorrentFileExists(options.hasWebtorrentFiles) | 180 | this.whereWebVideoFileExists(options.hasWebtorrentFiles) |
181 | } else if (exists(options.hasWebVideoFiles)) { | ||
182 | this.whereWebVideoFileExists(options.hasWebVideoFiles) | ||
179 | } | 183 | } |
180 | 184 | ||
181 | if (exists(options.hasHLSFiles)) { | 185 | if (exists(options.hasHLSFiles)) { |
@@ -400,18 +404,18 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { | |||
400 | } | 404 | } |
401 | 405 | ||
402 | private whereFileExists () { | 406 | private whereFileExists () { |
403 | this.and.push(`(${this.buildWebTorrentFileExistsQuery(true)} OR ${this.buildHLSFileExistsQuery(true)})`) | 407 | this.and.push(`(${this.buildWebVideoFileExistsQuery(true)} OR ${this.buildHLSFileExistsQuery(true)})`) |
404 | } | 408 | } |
405 | 409 | ||
406 | private whereWebTorrentFileExists (exists: boolean) { | 410 | private whereWebVideoFileExists (exists: boolean) { |
407 | this.and.push(this.buildWebTorrentFileExistsQuery(exists)) | 411 | this.and.push(this.buildWebVideoFileExistsQuery(exists)) |
408 | } | 412 | } |
409 | 413 | ||
410 | private whereHLSFileExists (exists: boolean) { | 414 | private whereHLSFileExists (exists: boolean) { |
411 | this.and.push(this.buildHLSFileExistsQuery(exists)) | 415 | this.and.push(this.buildHLSFileExistsQuery(exists)) |
412 | } | 416 | } |
413 | 417 | ||
414 | private buildWebTorrentFileExistsQuery (exists: boolean) { | 418 | private buildWebVideoFileExistsQuery (exists: boolean) { |
415 | const prefix = exists ? '' : 'NOT ' | 419 | const prefix = exists ? '' : 'NOT ' |
416 | 420 | ||
417 | return prefix + 'EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id")' | 421 | return prefix + 'EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id")' |
diff --git a/server/models/video/sql/video/videos-model-list-query-builder.ts b/server/models/video/sql/video/videos-model-list-query-builder.ts index 3fdac4ed3..b73dc28cd 100644 --- a/server/models/video/sql/video/videos-model-list-query-builder.ts +++ b/server/models/video/sql/video/videos-model-list-query-builder.ts | |||
@@ -18,7 +18,7 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder { | |||
18 | private innerQuery: string | 18 | private innerQuery: string |
19 | private innerSort: string | 19 | private innerSort: string |
20 | 20 | ||
21 | webtorrentFilesQueryBuilder: VideoFileQueryBuilder | 21 | webVideoFilesQueryBuilder: VideoFileQueryBuilder |
22 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder | 22 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder |
23 | 23 | ||
24 | private readonly videoModelBuilder: VideoModelBuilder | 24 | private readonly videoModelBuilder: VideoModelBuilder |
@@ -27,7 +27,7 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder { | |||
27 | super(sequelize, 'list') | 27 | super(sequelize, 'list') |
28 | 28 | ||
29 | this.videoModelBuilder = new VideoModelBuilder(this.mode, this.tables) | 29 | this.videoModelBuilder = new VideoModelBuilder(this.mode, this.tables) |
30 | this.webtorrentFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | 30 | this.webVideoFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) |
31 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | 31 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) |
32 | } | 32 | } |
33 | 33 | ||
@@ -48,12 +48,12 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder { | |||
48 | includeRedundancy: false | 48 | includeRedundancy: false |
49 | } | 49 | } |
50 | 50 | ||
51 | const [ rowsWebTorrentFiles, rowsStreamingPlaylist ] = await Promise.all([ | 51 | const [ rowsWebVideoFiles, rowsStreamingPlaylist ] = await Promise.all([ |
52 | this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(fileQueryOptions), | 52 | this.webVideoFilesQueryBuilder.queryWebVideos(fileQueryOptions), |
53 | this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(fileQueryOptions) | 53 | this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(fileQueryOptions) |
54 | ]) | 54 | ]) |
55 | 55 | ||
56 | return this.videoModelBuilder.buildVideosFromRows({ rows, include: options.include, rowsStreamingPlaylist, rowsWebTorrentFiles }) | 56 | return this.videoModelBuilder.buildVideosFromRows({ rows, include: options.include, rowsStreamingPlaylist, rowsWebVideoFiles }) |
57 | } | 57 | } |
58 | } | 58 | } |
59 | 59 | ||
diff --git a/server/models/video/storyboard.ts b/server/models/video/storyboard.ts new file mode 100644 index 000000000..65a044c98 --- /dev/null +++ b/server/models/video/storyboard.ts | |||
@@ -0,0 +1,169 @@ | |||
1 | import { remove } from 'fs-extra' | ||
2 | import { join } from 'path' | ||
3 | import { AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
5 | import { MStoryboard, MStoryboardVideo, MVideo } from '@server/types/models' | ||
6 | import { Storyboard } from '@shared/models' | ||
7 | import { AttributesOnly } from '@shared/typescript-utils' | ||
8 | import { logger } from '../../helpers/logger' | ||
9 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, WEBSERVER } from '../../initializers/constants' | ||
10 | import { VideoModel } from './video' | ||
11 | import { Transaction } from 'sequelize' | ||
12 | |||
13 | @Table({ | ||
14 | tableName: 'storyboard', | ||
15 | indexes: [ | ||
16 | { | ||
17 | fields: [ 'videoId' ], | ||
18 | unique: true | ||
19 | }, | ||
20 | { | ||
21 | fields: [ 'filename' ], | ||
22 | unique: true | ||
23 | } | ||
24 | ] | ||
25 | }) | ||
26 | export class StoryboardModel extends Model<Partial<AttributesOnly<StoryboardModel>>> { | ||
27 | |||
28 | @AllowNull(false) | ||
29 | @Column | ||
30 | filename: string | ||
31 | |||
32 | @AllowNull(false) | ||
33 | @Column | ||
34 | totalHeight: number | ||
35 | |||
36 | @AllowNull(false) | ||
37 | @Column | ||
38 | totalWidth: number | ||
39 | |||
40 | @AllowNull(false) | ||
41 | @Column | ||
42 | spriteHeight: number | ||
43 | |||
44 | @AllowNull(false) | ||
45 | @Column | ||
46 | spriteWidth: number | ||
47 | |||
48 | @AllowNull(false) | ||
49 | @Column | ||
50 | spriteDuration: number | ||
51 | |||
52 | @AllowNull(true) | ||
53 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max)) | ||
54 | fileUrl: string | ||
55 | |||
56 | @ForeignKey(() => VideoModel) | ||
57 | @Column | ||
58 | videoId: number | ||
59 | |||
60 | @BelongsTo(() => VideoModel, { | ||
61 | foreignKey: { | ||
62 | allowNull: true | ||
63 | }, | ||
64 | onDelete: 'CASCADE' | ||
65 | }) | ||
66 | Video: VideoModel | ||
67 | |||
68 | @CreatedAt | ||
69 | createdAt: Date | ||
70 | |||
71 | @UpdatedAt | ||
72 | updatedAt: Date | ||
73 | |||
74 | @AfterDestroy | ||
75 | static removeInstanceFile (instance: StoryboardModel) { | ||
76 | logger.info('Removing storyboard file %s.', instance.filename) | ||
77 | |||
78 | // Don't block the transaction | ||
79 | instance.removeFile() | ||
80 | .catch(err => logger.error('Cannot remove storyboard file %s.', instance.filename, { err })) | ||
81 | } | ||
82 | |||
83 | static loadByVideo (videoId: number, transaction?: Transaction): Promise<MStoryboard> { | ||
84 | const query = { | ||
85 | where: { | ||
86 | videoId | ||
87 | }, | ||
88 | transaction | ||
89 | } | ||
90 | |||
91 | return StoryboardModel.findOne(query) | ||
92 | } | ||
93 | |||
94 | static loadByFilename (filename: string): Promise<MStoryboard> { | ||
95 | const query = { | ||
96 | where: { | ||
97 | filename | ||
98 | } | ||
99 | } | ||
100 | |||
101 | return StoryboardModel.findOne(query) | ||
102 | } | ||
103 | |||
104 | static loadWithVideoByFilename (filename: string): Promise<MStoryboardVideo> { | ||
105 | const query = { | ||
106 | where: { | ||
107 | filename | ||
108 | }, | ||
109 | include: [ | ||
110 | { | ||
111 | model: VideoModel.unscoped(), | ||
112 | required: true | ||
113 | } | ||
114 | ] | ||
115 | } | ||
116 | |||
117 | return StoryboardModel.findOne(query) | ||
118 | } | ||
119 | |||
120 | // --------------------------------------------------------------------------- | ||
121 | |||
122 | static async listStoryboardsOf (video: MVideo): Promise<MStoryboardVideo[]> { | ||
123 | const query = { | ||
124 | where: { | ||
125 | videoId: video.id | ||
126 | } | ||
127 | } | ||
128 | |||
129 | const storyboards = await StoryboardModel.findAll<MStoryboard>(query) | ||
130 | |||
131 | return storyboards.map(s => Object.assign(s, { Video: video })) | ||
132 | } | ||
133 | |||
134 | // --------------------------------------------------------------------------- | ||
135 | |||
136 | getOriginFileUrl (video: MVideo) { | ||
137 | if (video.isOwned()) { | ||
138 | return WEBSERVER.URL + this.getLocalStaticPath() | ||
139 | } | ||
140 | |||
141 | return this.fileUrl | ||
142 | } | ||
143 | |||
144 | getLocalStaticPath () { | ||
145 | return LAZY_STATIC_PATHS.STORYBOARDS + this.filename | ||
146 | } | ||
147 | |||
148 | getPath () { | ||
149 | return join(CONFIG.STORAGE.STORYBOARDS_DIR, this.filename) | ||
150 | } | ||
151 | |||
152 | removeFile () { | ||
153 | return remove(this.getPath()) | ||
154 | } | ||
155 | |||
156 | toFormattedJSON (this: MStoryboardVideo): Storyboard { | ||
157 | return { | ||
158 | storyboardPath: this.getLocalStaticPath(), | ||
159 | |||
160 | totalHeight: this.totalHeight, | ||
161 | totalWidth: this.totalWidth, | ||
162 | |||
163 | spriteWidth: this.spriteWidth, | ||
164 | spriteHeight: this.spriteHeight, | ||
165 | |||
166 | spriteDuration: this.spriteDuration | ||
167 | } | ||
168 | } | ||
169 | } | ||
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts index a4ac581e5..1722acdb4 100644 --- a/server/models/video/thumbnail.ts +++ b/server/models/video/thumbnail.ts | |||
@@ -21,7 +21,7 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | 21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' |
22 | import { logger } from '../../helpers/logger' | 22 | import { logger } from '../../helpers/logger' |
23 | import { CONFIG } from '../../initializers/config' | 23 | import { CONFIG } from '../../initializers/config' |
24 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants' | 24 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, WEBSERVER } from '../../initializers/constants' |
25 | import { VideoModel } from './video' | 25 | import { VideoModel } from './video' |
26 | import { VideoPlaylistModel } from './video-playlist' | 26 | import { VideoPlaylistModel } from './video-playlist' |
27 | 27 | ||
@@ -69,6 +69,10 @@ export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel> | |||
69 | @Column | 69 | @Column |
70 | automaticallyGenerated: boolean | 70 | automaticallyGenerated: boolean |
71 | 71 | ||
72 | @AllowNull(false) | ||
73 | @Column | ||
74 | onDisk: boolean | ||
75 | |||
72 | @ForeignKey(() => VideoModel) | 76 | @ForeignKey(() => VideoModel) |
73 | @Column | 77 | @Column |
74 | videoId: number | 78 | videoId: number |
@@ -106,7 +110,7 @@ export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel> | |||
106 | [ThumbnailType.MINIATURE]: { | 110 | [ThumbnailType.MINIATURE]: { |
107 | label: 'miniature', | 111 | label: 'miniature', |
108 | directory: CONFIG.STORAGE.THUMBNAILS_DIR, | 112 | directory: CONFIG.STORAGE.THUMBNAILS_DIR, |
109 | staticPath: STATIC_PATHS.THUMBNAILS | 113 | staticPath: LAZY_STATIC_PATHS.THUMBNAILS |
110 | }, | 114 | }, |
111 | [ThumbnailType.PREVIEW]: { | 115 | [ThumbnailType.PREVIEW]: { |
112 | label: 'preview', | 116 | label: 'preview', |
@@ -197,4 +201,8 @@ export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel> | |||
197 | 201 | ||
198 | this.previousThumbnailFilename = undefined | 202 | this.previousThumbnailFilename = undefined |
199 | } | 203 | } |
204 | |||
205 | isOwned () { | ||
206 | return !this.fileUrl | ||
207 | } | ||
200 | } | 208 | } |
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts index 1fb1cae82..dd4cefd65 100644 --- a/server/models/video/video-caption.ts +++ b/server/models/video/video-caption.ts | |||
@@ -15,7 +15,7 @@ import { | |||
15 | Table, | 15 | Table, |
16 | UpdatedAt | 16 | UpdatedAt |
17 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
18 | import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models' | 18 | import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionLanguageUrl, MVideoCaptionVideo } from '@server/types/models' |
19 | import { buildUUID } from '@shared/extra-utils' | 19 | import { buildUUID } from '@shared/extra-utils' |
20 | import { AttributesOnly } from '@shared/typescript-utils' | 20 | import { AttributesOnly } from '@shared/typescript-utils' |
21 | import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' | 21 | import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' |
@@ -225,7 +225,7 @@ export class VideoCaptionModel extends Model<Partial<AttributesOnly<VideoCaption | |||
225 | } | 225 | } |
226 | } | 226 | } |
227 | 227 | ||
228 | getCaptionStaticPath (this: MVideoCaption) { | 228 | getCaptionStaticPath (this: MVideoCaptionLanguageUrl) { |
229 | return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.filename) | 229 | return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.filename) |
230 | } | 230 | } |
231 | 231 | ||
@@ -233,9 +233,7 @@ export class VideoCaptionModel extends Model<Partial<AttributesOnly<VideoCaption | |||
233 | return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename) | 233 | return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename) |
234 | } | 234 | } |
235 | 235 | ||
236 | getFileUrl (video: MVideo) { | 236 | getFileUrl (this: MVideoCaptionLanguageUrl, video: MVideo) { |
237 | if (!this.Video) this.Video = video as VideoModel | ||
238 | |||
239 | if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath() | 237 | if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath() |
240 | 238 | ||
241 | return this.fileUrl | 239 | return this.fileUrl |
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts index 2db4b523a..26f072f4f 100644 --- a/server/models/video/video-change-ownership.ts +++ b/server/models/video/video-change-ownership.ts | |||
@@ -45,7 +45,7 @@ enum ScopeNames { | |||
45 | { | 45 | { |
46 | model: VideoModel.scope([ | 46 | model: VideoModel.scope([ |
47 | VideoScopeNames.WITH_THUMBNAILS, | 47 | VideoScopeNames.WITH_THUMBNAILS, |
48 | VideoScopeNames.WITH_WEBTORRENT_FILES, | 48 | VideoScopeNames.WITH_WEB_VIDEO_FILES, |
49 | VideoScopeNames.WITH_STREAMING_PLAYLISTS, | 49 | VideoScopeNames.WITH_STREAMING_PLAYLISTS, |
50 | VideoScopeNames.WITH_ACCOUNT_DETAILS | 50 | VideoScopeNames.WITH_ACCOUNT_DETAILS |
51 | ]), | 51 | ]), |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 07bc13de1..ee34ad2ff 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -26,8 +26,8 @@ import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url' | |||
26 | import { | 26 | import { |
27 | getHLSPrivateFileUrl, | 27 | getHLSPrivateFileUrl, |
28 | getHLSPublicFileUrl, | 28 | getHLSPublicFileUrl, |
29 | getWebTorrentPrivateFileUrl, | 29 | getWebVideoPrivateFileUrl, |
30 | getWebTorrentPublicFileUrl | 30 | getWebVideoPublicFileUrl |
31 | } from '@server/lib/object-storage' | 31 | } from '@server/lib/object-storage' |
32 | import { getFSTorrentFilePath } from '@server/lib/paths' | 32 | import { getFSTorrentFilePath } from '@server/lib/paths' |
33 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' | 33 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' |
@@ -276,15 +276,15 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
276 | 276 | ||
277 | static async doesOwnedTorrentFileExist (filename: string) { | 277 | static async doesOwnedTorrentFileExist (filename: string) { |
278 | const query = 'SELECT 1 FROM "videoFile" ' + | 278 | const query = 'SELECT 1 FROM "videoFile" ' + |
279 | 'LEFT JOIN "video" "webtorrent" ON "webtorrent"."id" = "videoFile"."videoId" AND "webtorrent"."remote" IS FALSE ' + | 279 | 'LEFT JOIN "video" "webvideo" ON "webvideo"."id" = "videoFile"."videoId" AND "webvideo"."remote" IS FALSE ' + |
280 | 'LEFT JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' + | 280 | 'LEFT JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' + |
281 | 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + | 281 | 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + |
282 | 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1' | 282 | 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webvideo"."id" IS NOT NULL) LIMIT 1' |
283 | 283 | ||
284 | return doesExist(this.sequelize, query, { filename }) | 284 | return doesExist(this.sequelize, query, { filename }) |
285 | } | 285 | } |
286 | 286 | ||
287 | static async doesOwnedWebTorrentVideoFileExist (filename: string) { | 287 | static async doesOwnedWebVideoFileExist (filename: string) { |
288 | const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + | 288 | const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + |
289 | `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` | 289 | `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` |
290 | 290 | ||
@@ -378,7 +378,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
378 | } | 378 | } |
379 | 379 | ||
380 | static getStats () { | 380 | static getStats () { |
381 | const webtorrentFilesQuery: FindOptions = { | 381 | const webVideoFilesQuery: FindOptions = { |
382 | include: [ | 382 | include: [ |
383 | { | 383 | { |
384 | attributes: [], | 384 | attributes: [], |
@@ -412,10 +412,10 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
412 | } | 412 | } |
413 | 413 | ||
414 | return Promise.all([ | 414 | return Promise.all([ |
415 | VideoFileModel.aggregate('size', 'SUM', webtorrentFilesQuery), | 415 | VideoFileModel.aggregate('size', 'SUM', webVideoFilesQuery), |
416 | VideoFileModel.aggregate('size', 'SUM', hlsFilesQuery) | 416 | VideoFileModel.aggregate('size', 'SUM', hlsFilesQuery) |
417 | ]).then(([ webtorrentResult, hlsResult ]) => ({ | 417 | ]).then(([ webVideoResult, hlsResult ]) => ({ |
418 | totalLocalVideoFilesSize: parseAggregateResult(webtorrentResult) + parseAggregateResult(hlsResult) | 418 | totalLocalVideoFilesSize: parseAggregateResult(webVideoResult) + parseAggregateResult(hlsResult) |
419 | })) | 419 | })) |
420 | } | 420 | } |
421 | 421 | ||
@@ -433,7 +433,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
433 | 433 | ||
434 | const element = mode === 'streaming-playlist' | 434 | const element = mode === 'streaming-playlist' |
435 | ? await VideoFileModel.loadHLSFile({ ...baseFind, playlistId: videoFile.videoStreamingPlaylistId }) | 435 | ? await VideoFileModel.loadHLSFile({ ...baseFind, playlistId: videoFile.videoStreamingPlaylistId }) |
436 | : await VideoFileModel.loadWebTorrentFile({ ...baseFind, videoId: videoFile.videoId }) | 436 | : await VideoFileModel.loadWebVideoFile({ ...baseFind, videoId: videoFile.videoId }) |
437 | 437 | ||
438 | if (!element) return videoFile.save({ transaction }) | 438 | if (!element) return videoFile.save({ transaction }) |
439 | 439 | ||
@@ -444,7 +444,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
444 | return element.save({ transaction }) | 444 | return element.save({ transaction }) |
445 | } | 445 | } |
446 | 446 | ||
447 | static async loadWebTorrentFile (options: { | 447 | static async loadWebVideoFile (options: { |
448 | videoId: number | 448 | videoId: number |
449 | fps: number | 449 | fps: number |
450 | resolution: number | 450 | resolution: number |
@@ -523,7 +523,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
523 | return getHLSPrivateFileUrl(video, this.filename) | 523 | return getHLSPrivateFileUrl(video, this.filename) |
524 | } | 524 | } |
525 | 525 | ||
526 | return getWebTorrentPrivateFileUrl(this.filename) | 526 | return getWebVideoPrivateFileUrl(this.filename) |
527 | } | 527 | } |
528 | 528 | ||
529 | private getPublicObjectStorageUrl () { | 529 | private getPublicObjectStorageUrl () { |
@@ -531,7 +531,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
531 | return getHLSPublicFileUrl(this.fileUrl) | 531 | return getHLSPublicFileUrl(this.fileUrl) |
532 | } | 532 | } |
533 | 533 | ||
534 | return getWebTorrentPublicFileUrl(this.fileUrl) | 534 | return getWebVideoPublicFileUrl(this.fileUrl) |
535 | } | 535 | } |
536 | 536 | ||
537 | // --------------------------------------------------------------------------- | 537 | // --------------------------------------------------------------------------- |
@@ -553,15 +553,15 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
553 | getFileStaticPath (video: MVideo) { | 553 | getFileStaticPath (video: MVideo) { |
554 | if (this.isHLS()) return this.getHLSFileStaticPath(video) | 554 | if (this.isHLS()) return this.getHLSFileStaticPath(video) |
555 | 555 | ||
556 | return this.getWebTorrentFileStaticPath(video) | 556 | return this.getWebVideoFileStaticPath(video) |
557 | } | 557 | } |
558 | 558 | ||
559 | private getWebTorrentFileStaticPath (video: MVideo) { | 559 | private getWebVideoFileStaticPath (video: MVideo) { |
560 | if (isVideoInPrivateDirectory(video.privacy)) { | 560 | if (isVideoInPrivateDirectory(video.privacy)) { |
561 | return join(STATIC_PATHS.PRIVATE_WEBSEED, this.filename) | 561 | return join(STATIC_PATHS.PRIVATE_WEB_VIDEOS, this.filename) |
562 | } | 562 | } |
563 | 563 | ||
564 | return join(STATIC_PATHS.WEBSEED, this.filename) | 564 | return join(STATIC_PATHS.WEB_VIDEOS, this.filename) |
565 | } | 565 | } |
566 | 566 | ||
567 | private getHLSFileStaticPath (video: MVideo) { | 567 | private getHLSFileStaticPath (video: MVideo) { |
diff --git a/server/models/video/video-password.ts b/server/models/video/video-password.ts new file mode 100644 index 000000000..648366c3b --- /dev/null +++ b/server/models/video/video-password.ts | |||
@@ -0,0 +1,137 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DefaultScope, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { VideoModel } from './video' | ||
3 | import { AttributesOnly } from '@shared/typescript-utils' | ||
4 | import { ResultList, VideoPassword } from '@shared/models' | ||
5 | import { getSort, throwIfNotValid } from '../shared' | ||
6 | import { FindOptions, Transaction } from 'sequelize' | ||
7 | import { MVideoPassword } from '@server/types/models' | ||
8 | import { isPasswordValid } from '@server/helpers/custom-validators/videos' | ||
9 | import { pick } from '@shared/core-utils' | ||
10 | |||
11 | @DefaultScope(() => ({ | ||
12 | include: [ | ||
13 | { | ||
14 | model: VideoModel.unscoped(), | ||
15 | required: true | ||
16 | } | ||
17 | ] | ||
18 | })) | ||
19 | @Table({ | ||
20 | tableName: 'videoPassword', | ||
21 | indexes: [ | ||
22 | { | ||
23 | fields: [ 'videoId', 'password' ], | ||
24 | unique: true | ||
25 | } | ||
26 | ] | ||
27 | }) | ||
28 | export class VideoPasswordModel extends Model<Partial<AttributesOnly<VideoPasswordModel>>> { | ||
29 | |||
30 | @AllowNull(false) | ||
31 | @Is('VideoPassword', value => throwIfNotValid(value, isPasswordValid, 'videoPassword')) | ||
32 | @Column | ||
33 | password: string | ||
34 | |||
35 | @CreatedAt | ||
36 | createdAt: Date | ||
37 | |||
38 | @UpdatedAt | ||
39 | updatedAt: Date | ||
40 | |||
41 | @ForeignKey(() => VideoModel) | ||
42 | @Column | ||
43 | videoId: number | ||
44 | |||
45 | @BelongsTo(() => VideoModel, { | ||
46 | foreignKey: { | ||
47 | allowNull: false | ||
48 | }, | ||
49 | onDelete: 'cascade' | ||
50 | }) | ||
51 | Video: VideoModel | ||
52 | |||
53 | static async countByVideoId (videoId: number, t?: Transaction) { | ||
54 | const query: FindOptions = { | ||
55 | where: { | ||
56 | videoId | ||
57 | }, | ||
58 | transaction: t | ||
59 | } | ||
60 | |||
61 | return VideoPasswordModel.count(query) | ||
62 | } | ||
63 | |||
64 | static async loadByIdAndVideo (options: { id: number, videoId: number, t?: Transaction }): Promise<MVideoPassword> { | ||
65 | const { id, videoId, t } = options | ||
66 | const query: FindOptions = { | ||
67 | where: { | ||
68 | id, | ||
69 | videoId | ||
70 | }, | ||
71 | transaction: t | ||
72 | } | ||
73 | |||
74 | return VideoPasswordModel.findOne(query) | ||
75 | } | ||
76 | |||
77 | static async listPasswords (options: { | ||
78 | start: number | ||
79 | count: number | ||
80 | sort: string | ||
81 | videoId: number | ||
82 | }): Promise<ResultList<MVideoPassword>> { | ||
83 | const { start, count, sort, videoId } = options | ||
84 | |||
85 | const { count: total, rows: data } = await VideoPasswordModel.findAndCountAll({ | ||
86 | where: { videoId }, | ||
87 | order: getSort(sort), | ||
88 | offset: start, | ||
89 | limit: count | ||
90 | }) | ||
91 | |||
92 | return { total, data } | ||
93 | } | ||
94 | |||
95 | static async addPasswords (passwords: string[], videoId: number, transaction?: Transaction): Promise<void> { | ||
96 | for (const password of passwords) { | ||
97 | await VideoPasswordModel.create({ | ||
98 | password, | ||
99 | videoId | ||
100 | }, { transaction }) | ||
101 | } | ||
102 | } | ||
103 | |||
104 | static async deleteAllPasswords (videoId: number, transaction?: Transaction) { | ||
105 | await VideoPasswordModel.destroy({ | ||
106 | where: { videoId }, | ||
107 | transaction | ||
108 | }) | ||
109 | } | ||
110 | |||
111 | static async deletePassword (passwordId: number, transaction?: Transaction) { | ||
112 | await VideoPasswordModel.destroy({ | ||
113 | where: { id: passwordId }, | ||
114 | transaction | ||
115 | }) | ||
116 | } | ||
117 | |||
118 | static async isACorrectPassword (options: { | ||
119 | videoId: number | ||
120 | password: string | ||
121 | }) { | ||
122 | const query = { | ||
123 | where: pick(options, [ 'videoId', 'password' ]) | ||
124 | } | ||
125 | return VideoPasswordModel.findOne(query) | ||
126 | } | ||
127 | |||
128 | toFormattedJSON (): VideoPassword { | ||
129 | return { | ||
130 | id: this.id, | ||
131 | password: this.password, | ||
132 | videoId: this.videoId, | ||
133 | createdAt: this.createdAt, | ||
134 | updatedAt: this.updatedAt | ||
135 | } | ||
136 | } | ||
137 | } | ||
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index b832f9768..61ae6b9fe 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts | |||
@@ -336,7 +336,10 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide | |||
336 | // Internal video? | 336 | // Internal video? |
337 | if (video.privacy === VideoPrivacy.INTERNAL && accountId) return VideoPlaylistElementType.REGULAR | 337 | if (video.privacy === VideoPrivacy.INTERNAL && accountId) return VideoPlaylistElementType.REGULAR |
338 | 338 | ||
339 | if (video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL) return VideoPlaylistElementType.PRIVATE | 339 | // Private, internal and password protected videos cannot be read without appropriate access (ownership, internal) |
340 | if (new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL, VideoPrivacy.PASSWORD_PROTECTED ]).has(video.privacy)) { | ||
341 | return VideoPlaylistElementType.PRIVATE | ||
342 | } | ||
340 | 343 | ||
341 | if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE | 344 | if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE |
342 | 345 | ||
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index faf4bea78..15999d409 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -32,7 +32,7 @@ import { | |||
32 | import { | 32 | import { |
33 | ACTIVITY_PUB, | 33 | ACTIVITY_PUB, |
34 | CONSTRAINTS_FIELDS, | 34 | CONSTRAINTS_FIELDS, |
35 | STATIC_PATHS, | 35 | LAZY_STATIC_PATHS, |
36 | THUMBNAILS_SIZE, | 36 | THUMBNAILS_SIZE, |
37 | VIDEO_PLAYLIST_PRIVACIES, | 37 | VIDEO_PLAYLIST_PRIVACIES, |
38 | VIDEO_PLAYLIST_TYPES, | 38 | VIDEO_PLAYLIST_TYPES, |
@@ -592,13 +592,13 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
592 | getThumbnailUrl () { | 592 | getThumbnailUrl () { |
593 | if (!this.hasThumbnail()) return null | 593 | if (!this.hasThumbnail()) return null |
594 | 594 | ||
595 | return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename | 595 | return WEBSERVER.URL + LAZY_STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename |
596 | } | 596 | } |
597 | 597 | ||
598 | getThumbnailStaticPath () { | 598 | getThumbnailStaticPath () { |
599 | if (!this.hasThumbnail()) return null | 599 | if (!this.hasThumbnail()) return null |
600 | 600 | ||
601 | return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename) | 601 | return join(LAZY_STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename) |
602 | } | 602 | } |
603 | 603 | ||
604 | getWatchStaticPath () { | 604 | getWatchStaticPath () { |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 8e3af62a4..4c6297243 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -29,7 +29,7 @@ import { | |||
29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' | 29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' |
30 | import { InternalEventEmitter } from '@server/lib/internal-event-emitter' | 30 | import { InternalEventEmitter } from '@server/lib/internal-event-emitter' |
31 | import { LiveManager } from '@server/lib/live/live-manager' | 31 | import { LiveManager } from '@server/lib/live/live-manager' |
32 | import { removeHLSFileObjectStorageByFilename, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage' | 32 | import { removeHLSFileObjectStorageByFilename, removeHLSObjectStorage, removeWebVideoObjectStorage } from '@server/lib/object-storage' |
33 | import { tracer } from '@server/lib/opentelemetry/tracing' | 33 | import { tracer } from '@server/lib/opentelemetry/tracing' |
34 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' | 34 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' |
35 | import { Hooks } from '@server/lib/plugins/hooks' | 35 | import { Hooks } from '@server/lib/plugins/hooks' |
@@ -58,7 +58,7 @@ import { | |||
58 | import { AttributesOnly } from '@shared/typescript-utils' | 58 | import { AttributesOnly } from '@shared/typescript-utils' |
59 | import { peertubeTruncate } from '../../helpers/core-utils' | 59 | import { peertubeTruncate } from '../../helpers/core-utils' |
60 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 60 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
61 | import { exists, isBooleanValid, isUUIDValid } from '../../helpers/custom-validators/misc' | 61 | import { exists, isArray, isBooleanValid, isUUIDValid } from '../../helpers/custom-validators/misc' |
62 | import { | 62 | import { |
63 | isVideoDescriptionValid, | 63 | isVideoDescriptionValid, |
64 | isVideoDurationValid, | 64 | isVideoDurationValid, |
@@ -75,6 +75,7 @@ import { | |||
75 | MChannel, | 75 | MChannel, |
76 | MChannelAccountDefault, | 76 | MChannelAccountDefault, |
77 | MChannelId, | 77 | MChannelId, |
78 | MStoryboard, | ||
78 | MStreamingPlaylist, | 79 | MStreamingPlaylist, |
79 | MStreamingPlaylistFilesVideo, | 80 | MStreamingPlaylistFilesVideo, |
80 | MUserAccountId, | 81 | MUserAccountId, |
@@ -83,6 +84,8 @@ import { | |||
83 | MVideoAccountLight, | 84 | MVideoAccountLight, |
84 | MVideoAccountLightBlacklistAllFiles, | 85 | MVideoAccountLightBlacklistAllFiles, |
85 | MVideoAP, | 86 | MVideoAP, |
87 | MVideoAPLight, | ||
88 | MVideoCaptionLanguageUrl, | ||
86 | MVideoDetails, | 89 | MVideoDetails, |
87 | MVideoFileVideo, | 90 | MVideoFileVideo, |
88 | MVideoFormattable, | 91 | MVideoFormattable, |
@@ -111,13 +114,13 @@ import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, | |||
111 | import { UserModel } from '../user/user' | 114 | import { UserModel } from '../user/user' |
112 | import { UserVideoHistoryModel } from '../user/user-video-history' | 115 | import { UserVideoHistoryModel } from '../user/user-video-history' |
113 | import { VideoViewModel } from '../view/video-view' | 116 | import { VideoViewModel } from '../view/video-view' |
117 | import { videoModelToActivityPubObject } from './formatter/video-activity-pub-format' | ||
114 | import { | 118 | import { |
115 | videoFilesModelToFormattedJSON, | 119 | videoFilesModelToFormattedJSON, |
116 | VideoFormattingJSONOptions, | 120 | VideoFormattingJSONOptions, |
117 | videoModelToActivityPubObject, | ||
118 | videoModelToFormattedDetailsJSON, | 121 | videoModelToFormattedDetailsJSON, |
119 | videoModelToFormattedJSON | 122 | videoModelToFormattedJSON |
120 | } from './formatter/video-format-utils' | 123 | } from './formatter/video-api-format' |
121 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 124 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
122 | import { | 125 | import { |
123 | BuildVideosListQueryOptions, | 126 | BuildVideosListQueryOptions, |
@@ -126,6 +129,7 @@ import { | |||
126 | VideosIdListQueryBuilder, | 129 | VideosIdListQueryBuilder, |
127 | VideosModelListQueryBuilder | 130 | VideosModelListQueryBuilder |
128 | } from './sql/video' | 131 | } from './sql/video' |
132 | import { StoryboardModel } from './storyboard' | ||
129 | import { TagModel } from './tag' | 133 | import { TagModel } from './tag' |
130 | import { ThumbnailModel } from './thumbnail' | 134 | import { ThumbnailModel } from './thumbnail' |
131 | import { VideoBlacklistModel } from './video-blacklist' | 135 | import { VideoBlacklistModel } from './video-blacklist' |
@@ -136,6 +140,7 @@ import { VideoFileModel } from './video-file' | |||
136 | import { VideoImportModel } from './video-import' | 140 | import { VideoImportModel } from './video-import' |
137 | import { VideoJobInfoModel } from './video-job-info' | 141 | import { VideoJobInfoModel } from './video-job-info' |
138 | import { VideoLiveModel } from './video-live' | 142 | import { VideoLiveModel } from './video-live' |
143 | import { VideoPasswordModel } from './video-password' | ||
139 | import { VideoPlaylistElementModel } from './video-playlist-element' | 144 | import { VideoPlaylistElementModel } from './video-playlist-element' |
140 | import { VideoShareModel } from './video-share' | 145 | import { VideoShareModel } from './video-share' |
141 | import { VideoSourceModel } from './video-source' | 146 | import { VideoSourceModel } from './video-source' |
@@ -146,7 +151,7 @@ export enum ScopeNames { | |||
146 | FOR_API = 'FOR_API', | 151 | FOR_API = 'FOR_API', |
147 | WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', | 152 | WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', |
148 | WITH_TAGS = 'WITH_TAGS', | 153 | WITH_TAGS = 'WITH_TAGS', |
149 | WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES', | 154 | WITH_WEB_VIDEO_FILES = 'WITH_WEB_VIDEO_FILES', |
150 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', | 155 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', |
151 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', | 156 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', |
152 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', | 157 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', |
@@ -285,7 +290,7 @@ export type ForAPIOptions = { | |||
285 | } | 290 | } |
286 | ] | 291 | ] |
287 | }, | 292 | }, |
288 | [ScopeNames.WITH_WEBTORRENT_FILES]: (withRedundancies = false) => { | 293 | [ScopeNames.WITH_WEB_VIDEO_FILES]: (withRedundancies = false) => { |
289 | let subInclude: any[] = [] | 294 | let subInclude: any[] = [] |
290 | 295 | ||
291 | if (withRedundancies === true) { | 296 | if (withRedundancies === true) { |
@@ -734,6 +739,15 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
734 | }) | 739 | }) |
735 | VideoCaptions: VideoCaptionModel[] | 740 | VideoCaptions: VideoCaptionModel[] |
736 | 741 | ||
742 | @HasMany(() => VideoPasswordModel, { | ||
743 | foreignKey: { | ||
744 | name: 'videoId', | ||
745 | allowNull: false | ||
746 | }, | ||
747 | onDelete: 'cascade' | ||
748 | }) | ||
749 | VideoPasswords: VideoPasswordModel[] | ||
750 | |||
737 | @HasOne(() => VideoJobInfoModel, { | 751 | @HasOne(() => VideoJobInfoModel, { |
738 | foreignKey: { | 752 | foreignKey: { |
739 | name: 'videoId', | 753 | name: 'videoId', |
@@ -743,6 +757,16 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
743 | }) | 757 | }) |
744 | VideoJobInfo: VideoJobInfoModel | 758 | VideoJobInfo: VideoJobInfoModel |
745 | 759 | ||
760 | @HasOne(() => StoryboardModel, { | ||
761 | foreignKey: { | ||
762 | name: 'videoId', | ||
763 | allowNull: false | ||
764 | }, | ||
765 | onDelete: 'cascade', | ||
766 | hooks: true | ||
767 | }) | ||
768 | Storyboard: StoryboardModel | ||
769 | |||
746 | @AfterCreate | 770 | @AfterCreate |
747 | static notifyCreate (video: MVideo) { | 771 | static notifyCreate (video: MVideo) { |
748 | InternalEventEmitter.Instance.emit('video-created', { video }) | 772 | InternalEventEmitter.Instance.emit('video-created', { video }) |
@@ -789,7 +813,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
789 | 813 | ||
790 | // Remove physical files and torrents | 814 | // Remove physical files and torrents |
791 | instance.VideoFiles.forEach(file => { | 815 | instance.VideoFiles.forEach(file => { |
792 | tasks.push(instance.removeWebTorrentFile(file)) | 816 | tasks.push(instance.removeWebVideoFile(file)) |
793 | }) | 817 | }) |
794 | 818 | ||
795 | // Remove playlists file | 819 | // Remove playlists file |
@@ -894,6 +918,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
894 | required: false | 918 | required: false |
895 | }, | 919 | }, |
896 | { | 920 | { |
921 | model: StoryboardModel.unscoped(), | ||
922 | required: false | ||
923 | }, | ||
924 | { | ||
897 | attributes: [ 'id', 'url' ], | 925 | attributes: [ 'id', 'url' ], |
898 | model: VideoShareModel.unscoped(), | 926 | model: VideoShareModel.unscoped(), |
899 | required: false, | 927 | required: false, |
@@ -1079,7 +1107,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1079 | include?: VideoInclude | 1107 | include?: VideoInclude |
1080 | 1108 | ||
1081 | hasFiles?: boolean // default false | 1109 | hasFiles?: boolean // default false |
1082 | hasWebtorrentFiles?: boolean | 1110 | |
1111 | hasWebtorrentFiles?: boolean // TODO: remove in v7 | ||
1112 | hasWebVideoFiles?: boolean | ||
1113 | |||
1083 | hasHLSFiles?: boolean | 1114 | hasHLSFiles?: boolean |
1084 | 1115 | ||
1085 | categoryOneOf?: number[] | 1116 | categoryOneOf?: number[] |
@@ -1144,6 +1175,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1144 | 'historyOfUser', | 1175 | 'historyOfUser', |
1145 | 'hasHLSFiles', | 1176 | 'hasHLSFiles', |
1146 | 'hasWebtorrentFiles', | 1177 | 'hasWebtorrentFiles', |
1178 | 'hasWebVideoFiles', | ||
1147 | 'search', | 1179 | 'search', |
1148 | 'excludeAlreadyWatched' | 1180 | 'excludeAlreadyWatched' |
1149 | ]), | 1181 | ]), |
@@ -1177,7 +1209,9 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1177 | 1209 | ||
1178 | user?: MUserAccountId | 1210 | user?: MUserAccountId |
1179 | 1211 | ||
1180 | hasWebtorrentFiles?: boolean | 1212 | hasWebtorrentFiles?: boolean // TODO: remove in v7 |
1213 | hasWebVideoFiles?: boolean | ||
1214 | |||
1181 | hasHLSFiles?: boolean | 1215 | hasHLSFiles?: boolean |
1182 | 1216 | ||
1183 | search?: string | 1217 | search?: string |
@@ -1224,6 +1258,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1224 | 'durationMax', | 1258 | 'durationMax', |
1225 | 'hasHLSFiles', | 1259 | 'hasHLSFiles', |
1226 | 'hasWebtorrentFiles', | 1260 | 'hasWebtorrentFiles', |
1261 | 'hasWebVideoFiles', | ||
1227 | 'uuids', | 1262 | 'uuids', |
1228 | 'search', | 1263 | 'search', |
1229 | 'displayOnlyForFollower', | 1264 | 'displayOnlyForFollower', |
@@ -1648,7 +1683,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1648 | return this.getQualityFileBy(minBy) | 1683 | return this.getQualityFileBy(minBy) |
1649 | } | 1684 | } |
1650 | 1685 | ||
1651 | getWebTorrentFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo { | 1686 | getWebVideoFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo { |
1652 | if (Array.isArray(this.VideoFiles) === false) return undefined | 1687 | if (Array.isArray(this.VideoFiles) === false) return undefined |
1653 | 1688 | ||
1654 | const file = this.VideoFiles.find(f => f.resolution === resolution) | 1689 | const file = this.VideoFiles.find(f => f.resolution === resolution) |
@@ -1657,7 +1692,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1657 | return Object.assign(file, { Video: this }) | 1692 | return Object.assign(file, { Video: this }) |
1658 | } | 1693 | } |
1659 | 1694 | ||
1660 | hasWebTorrentFiles () { | 1695 | hasWebVideoFiles () { |
1661 | return Array.isArray(this.VideoFiles) === true && this.VideoFiles.length !== 0 | 1696 | return Array.isArray(this.VideoFiles) === true && this.VideoFiles.length !== 0 |
1662 | } | 1697 | } |
1663 | 1698 | ||
@@ -1758,6 +1793,32 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1758 | ) | 1793 | ) |
1759 | } | 1794 | } |
1760 | 1795 | ||
1796 | async lightAPToFullAP (this: MVideoAPLight, transaction: Transaction): Promise<MVideoAP> { | ||
1797 | const videoAP = this as MVideoAP | ||
1798 | |||
1799 | const getCaptions = () => { | ||
1800 | if (isArray(videoAP.VideoCaptions)) return videoAP.VideoCaptions | ||
1801 | |||
1802 | return this.$get('VideoCaptions', { | ||
1803 | attributes: [ 'filename', 'language', 'fileUrl' ], | ||
1804 | transaction | ||
1805 | }) as Promise<MVideoCaptionLanguageUrl[]> | ||
1806 | } | ||
1807 | |||
1808 | const getStoryboard = () => { | ||
1809 | if (videoAP.Storyboard) return videoAP.Storyboard | ||
1810 | |||
1811 | return this.$get('Storyboard', { transaction }) as Promise<MStoryboard> | ||
1812 | } | ||
1813 | |||
1814 | const [ captions, storyboard ] = await Promise.all([ getCaptions(), getStoryboard() ]) | ||
1815 | |||
1816 | return Object.assign(this, { | ||
1817 | VideoCaptions: captions, | ||
1818 | Storyboard: storyboard | ||
1819 | }) | ||
1820 | } | ||
1821 | |||
1761 | getTruncatedDescription () { | 1822 | getTruncatedDescription () { |
1762 | if (!this.description) return null | 1823 | if (!this.description) return null |
1763 | 1824 | ||
@@ -1830,7 +1891,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1830 | .concat(toAdd) | 1891 | .concat(toAdd) |
1831 | } | 1892 | } |
1832 | 1893 | ||
1833 | removeWebTorrentFile (videoFile: MVideoFile, isRedundancy = false) { | 1894 | removeWebVideoFile (videoFile: MVideoFile, isRedundancy = false) { |
1834 | const filePath = isRedundancy | 1895 | const filePath = isRedundancy |
1835 | ? VideoPathManager.Instance.getFSRedundancyVideoFilePath(this, videoFile) | 1896 | ? VideoPathManager.Instance.getFSRedundancyVideoFilePath(this, videoFile) |
1836 | : VideoPathManager.Instance.getFSVideoFileOutputPath(this, videoFile) | 1897 | : VideoPathManager.Instance.getFSVideoFileOutputPath(this, videoFile) |
@@ -1839,7 +1900,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1839 | if (!isRedundancy) promises.push(videoFile.removeTorrent()) | 1900 | if (!isRedundancy) promises.push(videoFile.removeTorrent()) |
1840 | 1901 | ||
1841 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { | 1902 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { |
1842 | promises.push(removeWebTorrentObjectStorage(videoFile)) | 1903 | promises.push(removeWebVideoObjectStorage(videoFile)) |
1843 | } | 1904 | } |
1844 | 1905 | ||
1845 | return Promise.all(promises) | 1906 | return Promise.all(promises) |
@@ -1918,7 +1979,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1918 | 1979 | ||
1919 | // --------------------------------------------------------------------------- | 1980 | // --------------------------------------------------------------------------- |
1920 | 1981 | ||
1921 | requiresAuth (options: { | 1982 | requiresUserAuth (options: { |
1922 | urlParamId: string | 1983 | urlParamId: string |
1923 | checkBlacklist: boolean | 1984 | checkBlacklist: boolean |
1924 | }) { | 1985 | }) { |
@@ -1936,11 +1997,11 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1936 | 1997 | ||
1937 | if (checkBlacklist && this.VideoBlacklist) return true | 1998 | if (checkBlacklist && this.VideoBlacklist) return true |
1938 | 1999 | ||
1939 | if (this.privacy !== VideoPrivacy.PUBLIC) { | 2000 | if (this.privacy === VideoPrivacy.PUBLIC || this.privacy === VideoPrivacy.PASSWORD_PROTECTED) { |
1940 | throw new Error(`Unknown video privacy ${this.privacy} to know if the video requires auth`) | 2001 | return false |
1941 | } | 2002 | } |
1942 | 2003 | ||
1943 | return false | 2004 | throw new Error(`Unknown video privacy ${this.privacy} to know if the video requires auth`) |
1944 | } | 2005 | } |
1945 | 2006 | ||
1946 | hasPrivateStaticPath () { | 2007 | hasPrivateStaticPath () { |
@@ -1962,7 +2023,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1962 | } | 2023 | } |
1963 | 2024 | ||
1964 | getBandwidthBits (this: MVideo, videoFile: MVideoFile) { | 2025 | getBandwidthBits (this: MVideo, videoFile: MVideoFile) { |
1965 | if (!this.duration) throw new Error(`Cannot get bandwidth bits because video ${this.url} has duration of 0`) | 2026 | if (!this.duration) return videoFile.size |
1966 | 2027 | ||
1967 | return Math.ceil((videoFile.size * 8) / this.duration) | 2028 | return Math.ceil((videoFile.size * 8) / this.duration) |
1968 | } | 2029 | } |
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 472cad182..80b616ccf 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -74,6 +74,9 @@ describe('Test config API validators', function () { | |||
74 | }, | 74 | }, |
75 | torrents: { | 75 | torrents: { |
76 | size: 4 | 76 | size: 4 |
77 | }, | ||
78 | storyboards: { | ||
79 | size: 5 | ||
77 | } | 80 | } |
78 | }, | 81 | }, |
79 | signup: { | 82 | signup: { |
@@ -123,7 +126,7 @@ describe('Test config API validators', function () { | |||
123 | '2160p': false | 126 | '2160p': false |
124 | }, | 127 | }, |
125 | alwaysTranscodeOriginalResolution: false, | 128 | alwaysTranscodeOriginalResolution: false, |
126 | webtorrent: { | 129 | webVideos: { |
127 | enabled: true | 130 | enabled: true |
128 | }, | 131 | }, |
129 | hls: { | 132 | hls: { |
@@ -342,7 +345,7 @@ describe('Test config API validators', function () { | |||
342 | }) | 345 | }) |
343 | }) | 346 | }) |
344 | 347 | ||
345 | it('Should fail with a disabled webtorrent & hls transcoding', async function () { | 348 | it('Should fail with a disabled web videos & hls transcoding', async function () { |
346 | const newUpdateParams = { | 349 | const newUpdateParams = { |
347 | ...updateParams, | 350 | ...updateParams, |
348 | 351 | ||
@@ -350,7 +353,7 @@ describe('Test config API validators', function () { | |||
350 | hls: { | 353 | hls: { |
351 | enabled: false | 354 | enabled: false |
352 | }, | 355 | }, |
353 | webtorrent: { | 356 | web_videos: { |
354 | enabled: false | 357 | enabled: false |
355 | } | 358 | } |
356 | } | 359 | } |
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index 400d312d3..c2a7ccd78 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -34,6 +34,7 @@ import './video-comments' | |||
34 | import './video-files' | 34 | import './video-files' |
35 | import './video-imports' | 35 | import './video-imports' |
36 | import './video-playlists' | 36 | import './video-playlists' |
37 | import './video-storyboards' | ||
37 | import './video-source' | 38 | import './video-source' |
38 | import './video-studio' | 39 | import './video-studio' |
39 | import './video-token' | 40 | import './video-token' |
diff --git a/server/tests/api/check-params/live.ts b/server/tests/api/check-params/live.ts index 2dc735c23..5021db516 100644 --- a/server/tests/api/check-params/live.ts +++ b/server/tests/api/check-params/live.ts | |||
@@ -143,7 +143,7 @@ describe('Test video lives API validator', function () { | |||
143 | }) | 143 | }) |
144 | 144 | ||
145 | it('Should fail with a bad privacy for replay settings', async function () { | 145 | it('Should fail with a bad privacy for replay settings', async function () { |
146 | const fields = { ...baseCorrectParams, replaySettings: { privacy: 5 } } | 146 | const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: 999 } } |
147 | 147 | ||
148 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 148 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
149 | }) | 149 | }) |
@@ -194,7 +194,7 @@ describe('Test video lives API validator', function () { | |||
194 | it('Should fail with a big thumbnail file', async function () { | 194 | it('Should fail with a big thumbnail file', async function () { |
195 | const fields = baseCorrectParams | 195 | const fields = baseCorrectParams |
196 | const attaches = { | 196 | const attaches = { |
197 | thumbnailfile: buildAbsoluteFixturePath('preview-big.png') | 197 | thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png') |
198 | } | 198 | } |
199 | 199 | ||
200 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 200 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
@@ -212,7 +212,7 @@ describe('Test video lives API validator', function () { | |||
212 | it('Should fail with a big preview file', async function () { | 212 | it('Should fail with a big preview file', async function () { |
213 | const fields = baseCorrectParams | 213 | const fields = baseCorrectParams |
214 | const attaches = { | 214 | const attaches = { |
215 | previewfile: buildAbsoluteFixturePath('preview-big.png') | 215 | previewfile: buildAbsoluteFixturePath('custom-preview-big.png') |
216 | } | 216 | } |
217 | 217 | ||
218 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 218 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
@@ -472,7 +472,7 @@ describe('Test video lives API validator', function () { | |||
472 | }) | 472 | }) |
473 | 473 | ||
474 | it('Should fail with a bad privacy for replay settings', async function () { | 474 | it('Should fail with a bad privacy for replay settings', async function () { |
475 | const fields = { saveReplay: true, replaySettings: { privacy: 5 } } | 475 | const fields = { saveReplay: true, replaySettings: { privacy: 999 } } |
476 | 476 | ||
477 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 477 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
478 | }) | 478 | }) |
diff --git a/server/tests/api/check-params/runners.ts b/server/tests/api/check-params/runners.ts index 48821b678..4ba90802f 100644 --- a/server/tests/api/check-params/runners.ts +++ b/server/tests/api/check-params/runners.ts | |||
@@ -752,7 +752,7 @@ describe('Test managing runners', function () { | |||
752 | }) | 752 | }) |
753 | 753 | ||
754 | it('Should fail with an invalid vod audio merge payload', async function () { | 754 | it('Should fail with an invalid vod audio merge payload', async function () { |
755 | const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } | 755 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } |
756 | await server.videos.upload({ attributes, mode: 'legacy' }) | 756 | await server.videos.upload({ attributes, mode: 'legacy' }) |
757 | 757 | ||
758 | await waitJobs([ server ]) | 758 | await waitJobs([ server ]) |
diff --git a/server/tests/api/check-params/transcoding.ts b/server/tests/api/check-params/transcoding.ts index 9846ac182..4bebcb528 100644 --- a/server/tests/api/check-params/transcoding.ts +++ b/server/tests/api/check-params/transcoding.ts | |||
@@ -49,21 +49,21 @@ describe('Test transcoding API validators', function () { | |||
49 | 49 | ||
50 | it('Should not run transcoding of a unknown video', async function () { | 50 | it('Should not run transcoding of a unknown video', async function () { |
51 | await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'hls', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 51 | await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'hls', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
52 | await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'webtorrent', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 52 | await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'web-video', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
53 | }) | 53 | }) |
54 | 54 | ||
55 | it('Should not run transcoding of a remote video', async function () { | 55 | it('Should not run transcoding of a remote video', async function () { |
56 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | 56 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 |
57 | 57 | ||
58 | await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'hls', expectedStatus }) | 58 | await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'hls', expectedStatus }) |
59 | await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'webtorrent', expectedStatus }) | 59 | await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'web-video', expectedStatus }) |
60 | }) | 60 | }) |
61 | 61 | ||
62 | it('Should not run transcoding by a non admin user', async function () { | 62 | it('Should not run transcoding by a non admin user', async function () { |
63 | const expectedStatus = HttpStatusCode.FORBIDDEN_403 | 63 | const expectedStatus = HttpStatusCode.FORBIDDEN_403 |
64 | 64 | ||
65 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', token: userToken, expectedStatus }) | 65 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', token: userToken, expectedStatus }) |
66 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'webtorrent', token: moderatorToken, expectedStatus }) | 66 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', token: moderatorToken, expectedStatus }) |
67 | }) | 67 | }) |
68 | 68 | ||
69 | it('Should not run transcoding without transcoding type', async function () { | 69 | it('Should not run transcoding without transcoding type', async function () { |
@@ -82,7 +82,7 @@ describe('Test transcoding API validators', function () { | |||
82 | await servers[0].config.disableTranscoding() | 82 | await servers[0].config.disableTranscoding() |
83 | 83 | ||
84 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', expectedStatus }) | 84 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', expectedStatus }) |
85 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'webtorrent', expectedStatus }) | 85 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus }) |
86 | }) | 86 | }) |
87 | 87 | ||
88 | it('Should run transcoding', async function () { | 88 | it('Should run transcoding', async function () { |
@@ -93,15 +93,15 @@ describe('Test transcoding API validators', function () { | |||
93 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls' }) | 93 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls' }) |
94 | await waitJobs(servers) | 94 | await waitJobs(servers) |
95 | 95 | ||
96 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'webtorrent' }) | 96 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video' }) |
97 | await waitJobs(servers) | 97 | await waitJobs(servers) |
98 | }) | 98 | }) |
99 | 99 | ||
100 | it('Should not run transcoding on a video that is already being transcoded', async function () { | 100 | it('Should not run transcoding on a video that is already being transcoded', async function () { |
101 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'webtorrent' }) | 101 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video' }) |
102 | 102 | ||
103 | const expectedStatus = HttpStatusCode.CONFLICT_409 | 103 | const expectedStatus = HttpStatusCode.CONFLICT_409 |
104 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'webtorrent', expectedStatus }) | 104 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus }) |
105 | }) | 105 | }) |
106 | 106 | ||
107 | after(async function () { | 107 | after(async function () { |
diff --git a/server/tests/api/check-params/video-files.ts b/server/tests/api/check-params/video-files.ts index 9dc59a1b5..4d43ab6f8 100644 --- a/server/tests/api/check-params/video-files.ts +++ b/server/tests/api/check-params/video-files.ts | |||
@@ -60,7 +60,7 @@ describe('Test videos files', function () { | |||
60 | }) | 60 | }) |
61 | 61 | ||
62 | describe('Deleting files', function () { | 62 | describe('Deleting files', function () { |
63 | let webtorrentId: string | 63 | let webVideoId: string |
64 | let hlsId: string | 64 | let hlsId: string |
65 | let remoteId: string | 65 | let remoteId: string |
66 | 66 | ||
@@ -68,10 +68,10 @@ describe('Test videos files', function () { | |||
68 | let validId2: string | 68 | let validId2: string |
69 | 69 | ||
70 | let hlsFileId: number | 70 | let hlsFileId: number |
71 | let webtorrentFileId: number | 71 | let webVideoFileId: number |
72 | 72 | ||
73 | let remoteHLSFileId: number | 73 | let remoteHLSFileId: number |
74 | let remoteWebtorrentFileId: number | 74 | let remoteWebVideoFileId: number |
75 | 75 | ||
76 | before(async function () { | 76 | before(async function () { |
77 | this.timeout(300_000) | 77 | this.timeout(300_000) |
@@ -83,7 +83,7 @@ describe('Test videos files', function () { | |||
83 | const video = await servers[1].videos.get({ id: uuid }) | 83 | const video = await servers[1].videos.get({ id: uuid }) |
84 | remoteId = video.uuid | 84 | remoteId = video.uuid |
85 | remoteHLSFileId = video.streamingPlaylists[0].files[0].id | 85 | remoteHLSFileId = video.streamingPlaylists[0].files[0].id |
86 | remoteWebtorrentFileId = video.files[0].id | 86 | remoteWebVideoFileId = video.files[0].id |
87 | } | 87 | } |
88 | 88 | ||
89 | { | 89 | { |
@@ -96,7 +96,7 @@ describe('Test videos files', function () { | |||
96 | const video = await servers[0].videos.get({ id: uuid }) | 96 | const video = await servers[0].videos.get({ id: uuid }) |
97 | validId1 = video.uuid | 97 | validId1 = video.uuid |
98 | hlsFileId = video.streamingPlaylists[0].files[0].id | 98 | hlsFileId = video.streamingPlaylists[0].files[0].id |
99 | webtorrentFileId = video.files[0].id | 99 | webVideoFileId = video.files[0].id |
100 | } | 100 | } |
101 | 101 | ||
102 | { | 102 | { |
@@ -117,8 +117,8 @@ describe('Test videos files', function () { | |||
117 | 117 | ||
118 | { | 118 | { |
119 | await servers[0].config.enableTranscoding(false, true) | 119 | await servers[0].config.enableTranscoding(false, true) |
120 | const { uuid } = await servers[0].videos.quickUpload({ name: 'webtorrent' }) | 120 | const { uuid } = await servers[0].videos.quickUpload({ name: 'web-video' }) |
121 | webtorrentId = uuid | 121 | webVideoId = uuid |
122 | } | 122 | } |
123 | 123 | ||
124 | await waitJobs(servers) | 124 | await waitJobs(servers) |
@@ -128,27 +128,27 @@ describe('Test videos files', function () { | |||
128 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 | 128 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 |
129 | 129 | ||
130 | await servers[0].videos.removeHLSPlaylist({ videoId: 404, expectedStatus }) | 130 | await servers[0].videos.removeHLSPlaylist({ videoId: 404, expectedStatus }) |
131 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: 404, expectedStatus }) | 131 | await servers[0].videos.removeAllWebVideoFiles({ videoId: 404, expectedStatus }) |
132 | 132 | ||
133 | await servers[0].videos.removeHLSFile({ videoId: 404, fileId: hlsFileId, expectedStatus }) | 133 | await servers[0].videos.removeHLSFile({ videoId: 404, fileId: hlsFileId, expectedStatus }) |
134 | await servers[0].videos.removeWebTorrentFile({ videoId: 404, fileId: webtorrentFileId, expectedStatus }) | 134 | await servers[0].videos.removeWebVideoFile({ videoId: 404, fileId: webVideoFileId, expectedStatus }) |
135 | }) | 135 | }) |
136 | 136 | ||
137 | it('Should not delete unknown files', async function () { | 137 | it('Should not delete unknown files', async function () { |
138 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 | 138 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 |
139 | 139 | ||
140 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: webtorrentFileId, expectedStatus }) | 140 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: webVideoFileId, expectedStatus }) |
141 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: hlsFileId, expectedStatus }) | 141 | await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: hlsFileId, expectedStatus }) |
142 | }) | 142 | }) |
143 | 143 | ||
144 | it('Should not delete files of a remote video', async function () { | 144 | it('Should not delete files of a remote video', async function () { |
145 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | 145 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 |
146 | 146 | ||
147 | await servers[0].videos.removeHLSPlaylist({ videoId: remoteId, expectedStatus }) | 147 | await servers[0].videos.removeHLSPlaylist({ videoId: remoteId, expectedStatus }) |
148 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: remoteId, expectedStatus }) | 148 | await servers[0].videos.removeAllWebVideoFiles({ videoId: remoteId, expectedStatus }) |
149 | 149 | ||
150 | await servers[0].videos.removeHLSFile({ videoId: remoteId, fileId: remoteHLSFileId, expectedStatus }) | 150 | await servers[0].videos.removeHLSFile({ videoId: remoteId, fileId: remoteHLSFileId, expectedStatus }) |
151 | await servers[0].videos.removeWebTorrentFile({ videoId: remoteId, fileId: remoteWebtorrentFileId, expectedStatus }) | 151 | await servers[0].videos.removeWebVideoFile({ videoId: remoteId, fileId: remoteWebVideoFileId, expectedStatus }) |
152 | }) | 152 | }) |
153 | 153 | ||
154 | it('Should not delete files by a non admin user', async function () { | 154 | it('Should not delete files by a non admin user', async function () { |
@@ -157,35 +157,35 @@ describe('Test videos files', function () { | |||
157 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: userToken, expectedStatus }) | 157 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: userToken, expectedStatus }) |
158 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: moderatorToken, expectedStatus }) | 158 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: moderatorToken, expectedStatus }) |
159 | 159 | ||
160 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1, token: userToken, expectedStatus }) | 160 | await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1, token: userToken, expectedStatus }) |
161 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1, token: moderatorToken, expectedStatus }) | 161 | await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1, token: moderatorToken, expectedStatus }) |
162 | 162 | ||
163 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: userToken, expectedStatus }) | 163 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: userToken, expectedStatus }) |
164 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: moderatorToken, expectedStatus }) | 164 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: moderatorToken, expectedStatus }) |
165 | 165 | ||
166 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId, token: userToken, expectedStatus }) | 166 | await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId, token: userToken, expectedStatus }) |
167 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId, token: moderatorToken, expectedStatus }) | 167 | await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId, token: moderatorToken, expectedStatus }) |
168 | }) | 168 | }) |
169 | 169 | ||
170 | it('Should not delete files if the files are not available', async function () { | 170 | it('Should not delete files if the files are not available', async function () { |
171 | await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 171 | await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
172 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 172 | await servers[0].videos.removeAllWebVideoFiles({ videoId: webVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
173 | 173 | ||
174 | await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 174 | await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
175 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 175 | await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
176 | }) | 176 | }) |
177 | 177 | ||
178 | it('Should not delete files if no both versions are available', async function () { | 178 | it('Should not delete files if no both versions are available', async function () { |
179 | await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 179 | await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
180 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 180 | await servers[0].videos.removeAllWebVideoFiles({ videoId: webVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
181 | }) | 181 | }) |
182 | 182 | ||
183 | it('Should delete files if both versions are available', async function () { | 183 | it('Should delete files if both versions are available', async function () { |
184 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId }) | 184 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId }) |
185 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId }) | 185 | await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId }) |
186 | 186 | ||
187 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1 }) | 187 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1 }) |
188 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId2 }) | 188 | await servers[0].videos.removeAllWebVideoFiles({ videoId: validId2 }) |
189 | }) | 189 | }) |
190 | }) | 190 | }) |
191 | 191 | ||
diff --git a/server/tests/api/check-params/video-imports.ts b/server/tests/api/check-params/video-imports.ts index 7f19b9ee9..8c6f43c12 100644 --- a/server/tests/api/check-params/video-imports.ts +++ b/server/tests/api/check-params/video-imports.ts | |||
@@ -244,7 +244,7 @@ describe('Test video imports API validator', function () { | |||
244 | it('Should fail with a big thumbnail file', async function () { | 244 | it('Should fail with a big thumbnail file', async function () { |
245 | const fields = baseCorrectParams | 245 | const fields = baseCorrectParams |
246 | const attaches = { | 246 | const attaches = { |
247 | thumbnailfile: buildAbsoluteFixturePath('preview-big.png') | 247 | thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png') |
248 | } | 248 | } |
249 | 249 | ||
250 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 250 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
@@ -262,7 +262,7 @@ describe('Test video imports API validator', function () { | |||
262 | it('Should fail with a big preview file', async function () { | 262 | it('Should fail with a big preview file', async function () { |
263 | const fields = baseCorrectParams | 263 | const fields = baseCorrectParams |
264 | const attaches = { | 264 | const attaches = { |
265 | previewfile: buildAbsoluteFixturePath('preview-big.png') | 265 | previewfile: buildAbsoluteFixturePath('custom-preview-big.png') |
266 | } | 266 | } |
267 | 267 | ||
268 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 268 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
diff --git a/server/tests/api/check-params/video-passwords.ts b/server/tests/api/check-params/video-passwords.ts new file mode 100644 index 000000000..4e936b5d2 --- /dev/null +++ b/server/tests/api/check-params/video-passwords.ts | |||
@@ -0,0 +1,609 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | import { | ||
3 | FIXTURE_URLS, | ||
4 | checkBadCountPagination, | ||
5 | checkBadSortPagination, | ||
6 | checkBadStartPagination, | ||
7 | checkUploadVideoParam | ||
8 | } from '@server/tests/shared' | ||
9 | import { root } from '@shared/core-utils' | ||
10 | import { | ||
11 | HttpStatusCode, | ||
12 | PeerTubeProblemDocument, | ||
13 | ServerErrorCode, | ||
14 | VideoCreateResult, | ||
15 | VideoPrivacy | ||
16 | } from '@shared/models' | ||
17 | import { | ||
18 | cleanupTests, | ||
19 | createSingleServer, | ||
20 | makePostBodyRequest, | ||
21 | PeerTubeServer, | ||
22 | setAccessTokensToServers | ||
23 | } from '@shared/server-commands' | ||
24 | import { expect } from 'chai' | ||
25 | import { join } from 'path' | ||
26 | |||
27 | describe('Test video passwords validator', function () { | ||
28 | let path: string | ||
29 | let server: PeerTubeServer | ||
30 | let userAccessToken = '' | ||
31 | let video: VideoCreateResult | ||
32 | let channelId: number | ||
33 | let publicVideo: VideoCreateResult | ||
34 | let commentId: number | ||
35 | // --------------------------------------------------------------- | ||
36 | |||
37 | before(async function () { | ||
38 | this.timeout(50000) | ||
39 | |||
40 | server = await createSingleServer(1) | ||
41 | |||
42 | await setAccessTokensToServers([ server ]) | ||
43 | |||
44 | await server.config.updateCustomSubConfig({ | ||
45 | newConfig: { | ||
46 | live: { | ||
47 | enabled: true, | ||
48 | latencySetting: { | ||
49 | enabled: false | ||
50 | }, | ||
51 | allowReplay: false | ||
52 | }, | ||
53 | import: { | ||
54 | videos: { | ||
55 | http:{ | ||
56 | enabled: true | ||
57 | } | ||
58 | } | ||
59 | } | ||
60 | } | ||
61 | }) | ||
62 | |||
63 | userAccessToken = await server.users.generateUserAndToken('user1') | ||
64 | |||
65 | { | ||
66 | const body = await server.users.getMyInfo() | ||
67 | channelId = body.videoChannels[0].id | ||
68 | } | ||
69 | |||
70 | { | ||
71 | video = await server.videos.quickUpload({ | ||
72 | name: 'password protected video', | ||
73 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
74 | videoPasswords: [ 'password1', 'password2' ] | ||
75 | }) | ||
76 | } | ||
77 | path = '/api/v1/videos/' | ||
78 | }) | ||
79 | |||
80 | async function checkVideoPasswordOptions (options: { | ||
81 | server: PeerTubeServer | ||
82 | token: string | ||
83 | videoPasswords: string[] | ||
84 | expectedStatus: HttpStatusCode | ||
85 | mode: 'uploadLegacy' | 'uploadResumable' | 'import' | 'updateVideo' | 'updatePasswords' | 'live' | ||
86 | }) { | ||
87 | const { server, token, videoPasswords, expectedStatus = HttpStatusCode.OK_200, mode } = options | ||
88 | const attaches = { | ||
89 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.webm') | ||
90 | } | ||
91 | const baseCorrectParams = { | ||
92 | name: 'my super name', | ||
93 | category: 5, | ||
94 | licence: 1, | ||
95 | language: 'pt', | ||
96 | nsfw: false, | ||
97 | commentsEnabled: true, | ||
98 | downloadEnabled: true, | ||
99 | waitTranscoding: true, | ||
100 | description: 'my super description', | ||
101 | support: 'my super support text', | ||
102 | tags: [ 'tag1', 'tag2' ], | ||
103 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
104 | channelId, | ||
105 | originallyPublishedAt: new Date().toISOString() | ||
106 | } | ||
107 | if (mode === 'uploadLegacy') { | ||
108 | const fields = { ...baseCorrectParams, videoPasswords } | ||
109 | return checkUploadVideoParam(server, token, { ...fields, ...attaches }, expectedStatus, 'legacy') | ||
110 | } | ||
111 | |||
112 | if (mode === 'uploadResumable') { | ||
113 | const fields = { ...baseCorrectParams, videoPasswords } | ||
114 | return checkUploadVideoParam(server, token, { ...fields, ...attaches }, expectedStatus, 'resumable') | ||
115 | } | ||
116 | |||
117 | if (mode === 'import') { | ||
118 | const attributes = { ...baseCorrectParams, targetUrl: FIXTURE_URLS.goodVideo, videoPasswords } | ||
119 | return server.imports.importVideo({ attributes, expectedStatus }) | ||
120 | } | ||
121 | |||
122 | if (mode === 'updateVideo') { | ||
123 | const attributes = { ...baseCorrectParams, videoPasswords } | ||
124 | return server.videos.update({ token, expectedStatus, id: video.id, attributes }) | ||
125 | } | ||
126 | |||
127 | if (mode === 'updatePasswords') { | ||
128 | return server.videoPasswords.updateAll({ token, expectedStatus, videoId: video.id, passwords: videoPasswords }) | ||
129 | } | ||
130 | |||
131 | if (mode === 'live') { | ||
132 | const fields = { ...baseCorrectParams, videoPasswords } | ||
133 | |||
134 | return server.live.create({ fields, expectedStatus }) | ||
135 | } | ||
136 | } | ||
137 | |||
138 | function validateVideoPasswordList (mode: 'uploadLegacy' | 'uploadResumable' | 'import' | 'updateVideo' | 'updatePasswords' | 'live') { | ||
139 | |||
140 | it('Should fail with a password protected privacy without providing a password', async function () { | ||
141 | await checkVideoPasswordOptions({ | ||
142 | server, | ||
143 | token: server.accessToken, | ||
144 | videoPasswords: undefined, | ||
145 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
146 | mode | ||
147 | }) | ||
148 | }) | ||
149 | |||
150 | it('Should fail with a password protected privacy and an empty password list', async function () { | ||
151 | const videoPasswords = [] | ||
152 | |||
153 | await checkVideoPasswordOptions({ | ||
154 | server, | ||
155 | token: server.accessToken, | ||
156 | videoPasswords, | ||
157 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
158 | mode | ||
159 | }) | ||
160 | }) | ||
161 | |||
162 | it('Should fail with a password protected privacy and a too short password', async function () { | ||
163 | const videoPasswords = [ 'p' ] | ||
164 | |||
165 | await checkVideoPasswordOptions({ | ||
166 | server, | ||
167 | token: server.accessToken, | ||
168 | videoPasswords, | ||
169 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
170 | mode | ||
171 | }) | ||
172 | }) | ||
173 | |||
174 | it('Should fail with a password protected privacy and a too long password', async function () { | ||
175 | const videoPasswords = [ 'Very very very very very very very very very very very very very very very very very very long password' ] | ||
176 | |||
177 | await checkVideoPasswordOptions({ | ||
178 | server, | ||
179 | token: server.accessToken, | ||
180 | videoPasswords, | ||
181 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
182 | mode | ||
183 | }) | ||
184 | }) | ||
185 | |||
186 | it('Should fail with a password protected privacy and an empty password', async function () { | ||
187 | const videoPasswords = [ '' ] | ||
188 | |||
189 | await checkVideoPasswordOptions({ | ||
190 | server, | ||
191 | token: server.accessToken, | ||
192 | videoPasswords, | ||
193 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
194 | mode | ||
195 | }) | ||
196 | }) | ||
197 | |||
198 | it('Should fail with a password protected privacy and duplicated passwords', async function () { | ||
199 | const videoPasswords = [ 'password', 'password' ] | ||
200 | |||
201 | await checkVideoPasswordOptions({ | ||
202 | server, | ||
203 | token: server.accessToken, | ||
204 | videoPasswords, | ||
205 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
206 | mode | ||
207 | }) | ||
208 | }) | ||
209 | |||
210 | if (mode === 'updatePasswords') { | ||
211 | it('Should fail for an unauthenticated user', async function () { | ||
212 | const videoPasswords = [ 'password' ] | ||
213 | await checkVideoPasswordOptions({ | ||
214 | server, | ||
215 | token: null, | ||
216 | videoPasswords, | ||
217 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401, | ||
218 | mode | ||
219 | }) | ||
220 | }) | ||
221 | |||
222 | it('Should fail for an unauthorized user', async function () { | ||
223 | const videoPasswords = [ 'password' ] | ||
224 | await checkVideoPasswordOptions({ | ||
225 | server, | ||
226 | token: userAccessToken, | ||
227 | videoPasswords, | ||
228 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
229 | mode | ||
230 | }) | ||
231 | }) | ||
232 | } | ||
233 | |||
234 | it('Should succeed with a password protected privacy and correct passwords', async function () { | ||
235 | const videoPasswords = [ 'password1', 'password2' ] | ||
236 | const expectedStatus = mode === 'updatePasswords' || mode === 'updateVideo' | ||
237 | ? HttpStatusCode.NO_CONTENT_204 | ||
238 | : HttpStatusCode.OK_200 | ||
239 | |||
240 | await checkVideoPasswordOptions({ server, token: server.accessToken, videoPasswords, expectedStatus, mode }) | ||
241 | }) | ||
242 | } | ||
243 | |||
244 | describe('When adding or updating a video', function () { | ||
245 | describe('Resumable upload', function () { | ||
246 | validateVideoPasswordList('uploadResumable') | ||
247 | }) | ||
248 | |||
249 | describe('Legacy upload', function () { | ||
250 | validateVideoPasswordList('uploadLegacy') | ||
251 | }) | ||
252 | |||
253 | describe('When importing a video', function () { | ||
254 | validateVideoPasswordList('import') | ||
255 | }) | ||
256 | |||
257 | describe('When updating a video', function () { | ||
258 | validateVideoPasswordList('updateVideo') | ||
259 | }) | ||
260 | |||
261 | describe('When updating the password list of a video', function () { | ||
262 | validateVideoPasswordList('updatePasswords') | ||
263 | }) | ||
264 | |||
265 | describe('When creating a live', function () { | ||
266 | validateVideoPasswordList('live') | ||
267 | }) | ||
268 | }) | ||
269 | |||
270 | async function checkVideoAccessOptions (options: { | ||
271 | server: PeerTubeServer | ||
272 | token?: string | ||
273 | videoPassword?: string | ||
274 | expectedStatus: HttpStatusCode | ||
275 | mode: 'get' | 'getWithPassword' | 'getWithToken' | 'listCaptions' | 'createThread' | 'listThreads' | 'replyThread' | 'rate' | 'token' | ||
276 | }) { | ||
277 | const { server, token = null, videoPassword, expectedStatus, mode } = options | ||
278 | |||
279 | if (mode === 'get') { | ||
280 | return server.videos.get({ id: video.id, expectedStatus }) | ||
281 | } | ||
282 | |||
283 | if (mode === 'getWithToken') { | ||
284 | return server.videos.getWithToken({ | ||
285 | id: video.id, | ||
286 | token, | ||
287 | expectedStatus | ||
288 | }) | ||
289 | } | ||
290 | |||
291 | if (mode === 'getWithPassword') { | ||
292 | return server.videos.getWithPassword({ | ||
293 | id: video.id, | ||
294 | token, | ||
295 | expectedStatus, | ||
296 | password: videoPassword | ||
297 | }) | ||
298 | } | ||
299 | |||
300 | if (mode === 'rate') { | ||
301 | return server.videos.rate({ | ||
302 | id: video.id, | ||
303 | token, | ||
304 | expectedStatus, | ||
305 | rating: 'like', | ||
306 | videoPassword | ||
307 | }) | ||
308 | } | ||
309 | |||
310 | if (mode === 'createThread') { | ||
311 | const fields = { text: 'super comment' } | ||
312 | const headers = videoPassword !== undefined && videoPassword !== null | ||
313 | ? { 'x-peertube-video-password': videoPassword } | ||
314 | : undefined | ||
315 | const body = await makePostBodyRequest({ | ||
316 | url: server.url, | ||
317 | path: path + video.uuid + '/comment-threads', | ||
318 | token, | ||
319 | fields, | ||
320 | headers, | ||
321 | expectedStatus | ||
322 | }) | ||
323 | return JSON.parse(body.text) | ||
324 | } | ||
325 | |||
326 | if (mode === 'replyThread') { | ||
327 | const fields = { text: 'super reply' } | ||
328 | const headers = videoPassword !== undefined && videoPassword !== null | ||
329 | ? { 'x-peertube-video-password': videoPassword } | ||
330 | : undefined | ||
331 | return makePostBodyRequest({ | ||
332 | url: server.url, | ||
333 | path: path + video.uuid + '/comments/' + commentId, | ||
334 | token, | ||
335 | fields, | ||
336 | headers, | ||
337 | expectedStatus | ||
338 | }) | ||
339 | } | ||
340 | if (mode === 'listThreads') { | ||
341 | return server.comments.listThreads({ | ||
342 | videoId: video.id, | ||
343 | token, | ||
344 | expectedStatus, | ||
345 | videoPassword | ||
346 | }) | ||
347 | } | ||
348 | |||
349 | if (mode === 'listCaptions') { | ||
350 | return server.captions.list({ | ||
351 | videoId: video.id, | ||
352 | token, | ||
353 | expectedStatus, | ||
354 | videoPassword | ||
355 | }) | ||
356 | } | ||
357 | |||
358 | if (mode === 'token') { | ||
359 | return server.videoToken.create({ | ||
360 | videoId: video.id, | ||
361 | token, | ||
362 | expectedStatus, | ||
363 | videoPassword | ||
364 | }) | ||
365 | } | ||
366 | } | ||
367 | |||
368 | function checkVideoError (error: any, mode: 'providePassword' | 'incorrectPassword') { | ||
369 | const serverCode = mode === 'providePassword' | ||
370 | ? ServerErrorCode.VIDEO_REQUIRES_PASSWORD | ||
371 | : ServerErrorCode.INCORRECT_VIDEO_PASSWORD | ||
372 | |||
373 | const message = mode === 'providePassword' | ||
374 | ? 'Please provide a password to access this password protected video' | ||
375 | : 'Incorrect video password. Access to the video is denied.' | ||
376 | |||
377 | if (!error.code) { | ||
378 | error = JSON.parse(error.text) | ||
379 | } | ||
380 | |||
381 | expect(error.code).to.equal(serverCode) | ||
382 | expect(error.detail).to.equal(message) | ||
383 | expect(error.error).to.equal(message) | ||
384 | |||
385 | expect(error.status).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
386 | } | ||
387 | |||
388 | function validateVideoAccess (mode: 'get' | 'listCaptions' | 'createThread' | 'listThreads' | 'replyThread' | 'rate' | 'token') { | ||
389 | const requiresUserAuth = [ 'createThread', 'replyThread', 'rate' ].includes(mode) | ||
390 | let tokens: string[] | ||
391 | if (!requiresUserAuth) { | ||
392 | it('Should fail without providing a password for an unlogged user', async function () { | ||
393 | const body = await checkVideoAccessOptions({ server, expectedStatus: HttpStatusCode.FORBIDDEN_403, mode }) | ||
394 | const error = body as unknown as PeerTubeProblemDocument | ||
395 | |||
396 | checkVideoError(error, 'providePassword') | ||
397 | }) | ||
398 | } | ||
399 | |||
400 | it('Should fail without providing a password for an unauthorised user', async function () { | ||
401 | const tmp = mode === 'get' ? 'getWithToken' : mode | ||
402 | |||
403 | const body = await checkVideoAccessOptions({ | ||
404 | server, | ||
405 | token: userAccessToken, | ||
406 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
407 | mode: tmp | ||
408 | }) | ||
409 | |||
410 | const error = body as unknown as PeerTubeProblemDocument | ||
411 | |||
412 | checkVideoError(error, 'providePassword') | ||
413 | }) | ||
414 | |||
415 | it('Should fail if a wrong password is entered', async function () { | ||
416 | const tmp = mode === 'get' ? 'getWithPassword' : mode | ||
417 | tokens = [ userAccessToken, server.accessToken ] | ||
418 | |||
419 | if (!requiresUserAuth) tokens.push(null) | ||
420 | |||
421 | for (const token of tokens) { | ||
422 | const body = await checkVideoAccessOptions({ | ||
423 | server, | ||
424 | token, | ||
425 | videoPassword: 'toto', | ||
426 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
427 | mode: tmp | ||
428 | }) | ||
429 | const error = body as unknown as PeerTubeProblemDocument | ||
430 | |||
431 | checkVideoError(error, 'incorrectPassword') | ||
432 | } | ||
433 | }) | ||
434 | |||
435 | it('Should fail if an empty password is entered', async function () { | ||
436 | const tmp = mode === 'get' ? 'getWithPassword' : mode | ||
437 | |||
438 | for (const token of tokens) { | ||
439 | const body = await checkVideoAccessOptions({ | ||
440 | server, | ||
441 | token, | ||
442 | videoPassword: '', | ||
443 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
444 | mode: tmp | ||
445 | }) | ||
446 | const error = body as unknown as PeerTubeProblemDocument | ||
447 | |||
448 | checkVideoError(error, 'incorrectPassword') | ||
449 | } | ||
450 | }) | ||
451 | |||
452 | it('Should fail if an inccorect password containing the correct password is entered', async function () { | ||
453 | const tmp = mode === 'get' ? 'getWithPassword' : mode | ||
454 | |||
455 | for (const token of tokens) { | ||
456 | const body = await checkVideoAccessOptions({ | ||
457 | server, | ||
458 | token, | ||
459 | videoPassword: 'password11', | ||
460 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
461 | mode: tmp | ||
462 | }) | ||
463 | const error = body as unknown as PeerTubeProblemDocument | ||
464 | |||
465 | checkVideoError(error, 'incorrectPassword') | ||
466 | } | ||
467 | }) | ||
468 | |||
469 | it('Should succeed without providing a password for an authorised user', async function () { | ||
470 | const tmp = mode === 'get' ? 'getWithToken' : mode | ||
471 | const expectedStatus = mode === 'rate' ? HttpStatusCode.NO_CONTENT_204 : HttpStatusCode.OK_200 | ||
472 | |||
473 | const body = await checkVideoAccessOptions({ server, token: server.accessToken, expectedStatus, mode: tmp }) | ||
474 | |||
475 | if (mode === 'createThread') commentId = body.comment.id | ||
476 | }) | ||
477 | |||
478 | it('Should succeed using correct passwords', async function () { | ||
479 | const tmp = mode === 'get' ? 'getWithPassword' : mode | ||
480 | const expectedStatus = mode === 'rate' ? HttpStatusCode.NO_CONTENT_204 : HttpStatusCode.OK_200 | ||
481 | |||
482 | for (const token of tokens) { | ||
483 | await checkVideoAccessOptions({ server, videoPassword: 'password1', token, expectedStatus, mode: tmp }) | ||
484 | await checkVideoAccessOptions({ server, videoPassword: 'password2', token, expectedStatus, mode: tmp }) | ||
485 | } | ||
486 | }) | ||
487 | } | ||
488 | |||
489 | describe('When accessing password protected video', function () { | ||
490 | |||
491 | describe('For getting a password protected video', function () { | ||
492 | validateVideoAccess('get') | ||
493 | }) | ||
494 | |||
495 | describe('For rating a video', function () { | ||
496 | validateVideoAccess('rate') | ||
497 | }) | ||
498 | |||
499 | describe('For creating a thread', function () { | ||
500 | validateVideoAccess('createThread') | ||
501 | }) | ||
502 | |||
503 | describe('For replying to a thread', function () { | ||
504 | validateVideoAccess('replyThread') | ||
505 | }) | ||
506 | |||
507 | describe('For listing threads', function () { | ||
508 | validateVideoAccess('listThreads') | ||
509 | }) | ||
510 | |||
511 | describe('For getting captions', function () { | ||
512 | validateVideoAccess('listCaptions') | ||
513 | }) | ||
514 | |||
515 | describe('For creating video file token', function () { | ||
516 | validateVideoAccess('token') | ||
517 | }) | ||
518 | }) | ||
519 | |||
520 | describe('When listing passwords', function () { | ||
521 | it('Should fail with a bad start pagination', async function () { | ||
522 | await checkBadStartPagination(server.url, path + video.uuid + '/passwords', server.accessToken) | ||
523 | }) | ||
524 | |||
525 | it('Should fail with a bad count pagination', async function () { | ||
526 | await checkBadCountPagination(server.url, path + video.uuid + '/passwords', server.accessToken) | ||
527 | }) | ||
528 | |||
529 | it('Should fail with an incorrect sort', async function () { | ||
530 | await checkBadSortPagination(server.url, path + video.uuid + '/passwords', server.accessToken) | ||
531 | }) | ||
532 | |||
533 | it('Should fail for unauthenticated user', async function () { | ||
534 | await server.videoPasswords.list({ | ||
535 | token: null, | ||
536 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401, | ||
537 | videoId: video.id | ||
538 | }) | ||
539 | }) | ||
540 | |||
541 | it('Should fail for unauthorized user', async function () { | ||
542 | await server.videoPasswords.list({ | ||
543 | token: userAccessToken, | ||
544 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
545 | videoId: video.id | ||
546 | }) | ||
547 | }) | ||
548 | |||
549 | it('Should succeed with the correct parameters', async function () { | ||
550 | await server.videoPasswords.list({ | ||
551 | token: server.accessToken, | ||
552 | expectedStatus: HttpStatusCode.OK_200, | ||
553 | videoId: video.id | ||
554 | }) | ||
555 | }) | ||
556 | }) | ||
557 | |||
558 | describe('When deleting a password', async function () { | ||
559 | const passwords = (await server.videoPasswords.list({ videoId: video.id })).data | ||
560 | |||
561 | it('Should fail with wrong password id', async function () { | ||
562 | await server.videoPasswords.remove({ id: -1, videoId: video.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
563 | }) | ||
564 | |||
565 | it('Should fail for unauthenticated user', async function () { | ||
566 | await server.videoPasswords.remove({ | ||
567 | id: passwords[0].id, | ||
568 | token: null, | ||
569 | videoId: video.id, | ||
570 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
571 | }) | ||
572 | }) | ||
573 | |||
574 | it('Should fail for unauthorized user', async function () { | ||
575 | await server.videoPasswords.remove({ | ||
576 | id: passwords[0].id, | ||
577 | token: userAccessToken, | ||
578 | videoId: video.id, | ||
579 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
580 | }) | ||
581 | }) | ||
582 | |||
583 | it('Should fail for non password protected video', async function () { | ||
584 | publicVideo = await server.videos.quickUpload({ name: 'public video' }) | ||
585 | await server.videoPasswords.remove({ id: passwords[0].id, videoId: publicVideo.id, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
586 | }) | ||
587 | |||
588 | it('Should fail for password not linked to correct video', async function () { | ||
589 | const video2 = await server.videos.quickUpload({ | ||
590 | name: 'password protected video', | ||
591 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
592 | videoPasswords: [ 'password1', 'password2' ] | ||
593 | }) | ||
594 | await server.videoPasswords.remove({ id: passwords[0].id, videoId: video2.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
595 | }) | ||
596 | |||
597 | it('Should succeed with correct parameter', async function () { | ||
598 | await server.videoPasswords.remove({ id: passwords[0].id, videoId: video.id, expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | ||
599 | }) | ||
600 | |||
601 | it('Should fail for last password of a video', async function () { | ||
602 | await server.videoPasswords.remove({ id: passwords[1].id, videoId: video.id, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
603 | }) | ||
604 | }) | ||
605 | |||
606 | after(async function () { | ||
607 | await cleanupTests([ server ]) | ||
608 | }) | ||
609 | }) | ||
diff --git a/server/tests/api/check-params/video-playlists.ts b/server/tests/api/check-params/video-playlists.ts index 8090897c1..8c3233e0b 100644 --- a/server/tests/api/check-params/video-playlists.ts +++ b/server/tests/api/check-params/video-playlists.ts | |||
@@ -196,7 +196,7 @@ describe('Test video playlists API validator', function () { | |||
196 | attributes: { | 196 | attributes: { |
197 | displayName: 'display name', | 197 | displayName: 'display name', |
198 | privacy: VideoPlaylistPrivacy.UNLISTED, | 198 | privacy: VideoPlaylistPrivacy.UNLISTED, |
199 | thumbnailfile: 'thumbnail.jpg', | 199 | thumbnailfile: 'custom-thumbnail.jpg', |
200 | videoChannelId: server.store.channel.id, | 200 | videoChannelId: server.store.channel.id, |
201 | 201 | ||
202 | ...attributes | 202 | ...attributes |
@@ -260,7 +260,7 @@ describe('Test video playlists API validator', function () { | |||
260 | }) | 260 | }) |
261 | 261 | ||
262 | it('Should fail with a thumbnail file too big', async function () { | 262 | it('Should fail with a thumbnail file too big', async function () { |
263 | const params = getBase({ thumbnailfile: 'preview-big.png' }) | 263 | const params = getBase({ thumbnailfile: 'custom-preview-big.png' }) |
264 | 264 | ||
265 | await command.create(params) | 265 | await command.create(params) |
266 | await command.update(getUpdate(params, playlist.shortUUID)) | 266 | await command.update(getUpdate(params, playlist.shortUUID)) |
diff --git a/server/tests/api/check-params/video-storyboards.ts b/server/tests/api/check-params/video-storyboards.ts new file mode 100644 index 000000000..c038e7370 --- /dev/null +++ b/server/tests/api/check-params/video-storyboards.ts | |||
@@ -0,0 +1,45 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { HttpStatusCode, VideoPrivacy } from '@shared/models' | ||
4 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | ||
5 | |||
6 | describe('Test video storyboards API validator', function () { | ||
7 | let server: PeerTubeServer | ||
8 | |||
9 | let publicVideo: { uuid: string } | ||
10 | let privateVideo: { uuid: string } | ||
11 | |||
12 | // --------------------------------------------------------------- | ||
13 | |||
14 | before(async function () { | ||
15 | this.timeout(120000) | ||
16 | |||
17 | server = await createSingleServer(1) | ||
18 | await setAccessTokensToServers([ server ]) | ||
19 | |||
20 | publicVideo = await server.videos.quickUpload({ name: 'public' }) | ||
21 | privateVideo = await server.videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE }) | ||
22 | }) | ||
23 | |||
24 | it('Should fail without a valid uuid', async function () { | ||
25 | await server.storyboard.list({ id: '4da6fde3-88f7-4d16-b119-108df563d0b0', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
26 | }) | ||
27 | |||
28 | it('Should receive 404 when passing a non existing video id', async function () { | ||
29 | await server.storyboard.list({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
30 | }) | ||
31 | |||
32 | it('Should not get the private storyboard without the appropriate token', async function () { | ||
33 | await server.storyboard.list({ id: privateVideo.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, token: null }) | ||
34 | await server.storyboard.list({ id: publicVideo.uuid, expectedStatus: HttpStatusCode.OK_200, token: null }) | ||
35 | }) | ||
36 | |||
37 | it('Should succeed with the correct parameters', async function () { | ||
38 | await server.storyboard.list({ id: privateVideo.uuid }) | ||
39 | await server.storyboard.list({ id: publicVideo.uuid }) | ||
40 | }) | ||
41 | |||
42 | after(async function () { | ||
43 | await cleanupTests([ server ]) | ||
44 | }) | ||
45 | }) | ||
diff --git a/server/tests/api/check-params/video-studio.ts b/server/tests/api/check-params/video-studio.ts index add8d9164..4ac0d93ed 100644 --- a/server/tests/api/check-params/video-studio.ts +++ b/server/tests/api/check-params/video-studio.ts | |||
@@ -293,7 +293,7 @@ describe('Test video studio API validator', function () { | |||
293 | it('Should succeed with the correct params', async function () { | 293 | it('Should succeed with the correct params', async function () { |
294 | this.timeout(120000) | 294 | this.timeout(120000) |
295 | 295 | ||
296 | await addWatermark('thumbnail.jpg', HttpStatusCode.NO_CONTENT_204) | 296 | await addWatermark('custom-thumbnail.jpg', HttpStatusCode.NO_CONTENT_204) |
297 | 297 | ||
298 | await waitJobs([ server ]) | 298 | await waitJobs([ server ]) |
299 | }) | 299 | }) |
@@ -322,8 +322,8 @@ describe('Test video studio API validator', function () { | |||
322 | }) | 322 | }) |
323 | 323 | ||
324 | it('Should fail with an invalid file', async function () { | 324 | it('Should fail with an invalid file', async function () { |
325 | await addIntroOutro('add-intro', 'thumbnail.jpg') | 325 | await addIntroOutro('add-intro', 'custom-thumbnail.jpg') |
326 | await addIntroOutro('add-outro', 'thumbnail.jpg') | 326 | await addIntroOutro('add-outro', 'custom-thumbnail.jpg') |
327 | }) | 327 | }) |
328 | 328 | ||
329 | it('Should fail with a file that does not contain video stream', async function () { | 329 | it('Should fail with a file that does not contain video stream', async function () { |
diff --git a/server/tests/api/check-params/video-token.ts b/server/tests/api/check-params/video-token.ts index 7acb9d580..7cb3e84a2 100644 --- a/server/tests/api/check-params/video-token.ts +++ b/server/tests/api/check-params/video-token.ts | |||
@@ -5,9 +5,12 @@ import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServ | |||
5 | 5 | ||
6 | describe('Test video tokens', function () { | 6 | describe('Test video tokens', function () { |
7 | let server: PeerTubeServer | 7 | let server: PeerTubeServer |
8 | let videoId: string | 8 | let privateVideoId: string |
9 | let passwordProtectedVideoId: string | ||
9 | let userToken: string | 10 | let userToken: string |
10 | 11 | ||
12 | const videoPassword = 'password' | ||
13 | |||
11 | // --------------------------------------------------------------- | 14 | // --------------------------------------------------------------- |
12 | 15 | ||
13 | before(async function () { | 16 | before(async function () { |
@@ -15,27 +18,50 @@ describe('Test video tokens', function () { | |||
15 | 18 | ||
16 | server = await createSingleServer(1) | 19 | server = await createSingleServer(1) |
17 | await setAccessTokensToServers([ server ]) | 20 | await setAccessTokensToServers([ server ]) |
18 | 21 | { | |
19 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | 22 | const { uuid } = await server.videos.quickUpload({ name: 'private video', privacy: VideoPrivacy.PRIVATE }) |
20 | videoId = uuid | 23 | privateVideoId = uuid |
21 | 24 | } | |
25 | { | ||
26 | const { uuid } = await server.videos.quickUpload({ | ||
27 | name: 'password protected video', | ||
28 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
29 | videoPasswords: [ videoPassword ] | ||
30 | }) | ||
31 | passwordProtectedVideoId = uuid | ||
32 | } | ||
22 | userToken = await server.users.generateUserAndToken('user1') | 33 | userToken = await server.users.generateUserAndToken('user1') |
23 | }) | 34 | }) |
24 | 35 | ||
25 | it('Should not generate tokens for unauthenticated user', async function () { | 36 | it('Should not generate tokens on private video for unauthenticated user', async function () { |
26 | await server.videoToken.create({ videoId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | 37 | await server.videoToken.create({ videoId: privateVideoId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) |
27 | }) | 38 | }) |
28 | 39 | ||
29 | it('Should not generate tokens of unknown video', async function () { | 40 | it('Should not generate tokens of unknown video', async function () { |
30 | await server.videoToken.create({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 41 | await server.videoToken.create({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
31 | }) | 42 | }) |
32 | 43 | ||
44 | it('Should not generate tokens with incorrect password', async function () { | ||
45 | await server.videoToken.create({ | ||
46 | videoId: passwordProtectedVideoId, | ||
47 | token: null, | ||
48 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
49 | videoPassword: 'incorrectPassword' | ||
50 | }) | ||
51 | }) | ||
52 | |||
33 | it('Should not generate tokens of a non owned video', async function () { | 53 | it('Should not generate tokens of a non owned video', async function () { |
34 | await server.videoToken.create({ videoId, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 54 | await server.videoToken.create({ videoId: privateVideoId, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
35 | }) | 55 | }) |
36 | 56 | ||
37 | it('Should generate token', async function () { | 57 | it('Should generate token', async function () { |
38 | await server.videoToken.create({ videoId }) | 58 | await server.videoToken.create({ videoId: privateVideoId }) |
59 | }) | ||
60 | |||
61 | it('Should generate token on password protected video', async function () { | ||
62 | await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword, token: null }) | ||
63 | await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword, token: userToken }) | ||
64 | await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword }) | ||
39 | }) | 65 | }) |
40 | 66 | ||
41 | after(async function () { | 67 | after(async function () { |
diff --git a/server/tests/api/check-params/videos-overviews.ts b/server/tests/api/check-params/videos-overviews.ts index f9cdb7ab3..ae7de24dd 100644 --- a/server/tests/api/check-params/videos-overviews.ts +++ b/server/tests/api/check-params/videos-overviews.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import { cleanupTests, createSingleServer, PeerTubeServer } from '@shared/server-commands' | 3 | import { cleanupTests, createSingleServer, PeerTubeServer } from '@shared/server-commands' |
4 | 4 | ||
5 | describe('Test videos overview', function () { | 5 | describe('Test videos overview API validator', function () { |
6 | let server: PeerTubeServer | 6 | let server: PeerTubeServer |
7 | 7 | ||
8 | // --------------------------------------------------------------- | 8 | // --------------------------------------------------------------- |
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index 094ab6891..6ee1955a7 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts | |||
@@ -384,7 +384,7 @@ describe('Test videos API validator', function () { | |||
384 | it('Should fail with a big thumbnail file', async function () { | 384 | it('Should fail with a big thumbnail file', async function () { |
385 | const fields = baseCorrectParams | 385 | const fields = baseCorrectParams |
386 | const attaches = { | 386 | const attaches = { |
387 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'), | 387 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'custom-preview-big.png'), |
388 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') | 388 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') |
389 | } | 389 | } |
390 | 390 | ||
@@ -404,7 +404,7 @@ describe('Test videos API validator', function () { | |||
404 | it('Should fail with a big preview file', async function () { | 404 | it('Should fail with a big preview file', async function () { |
405 | const fields = baseCorrectParams | 405 | const fields = baseCorrectParams |
406 | const attaches = { | 406 | const attaches = { |
407 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'), | 407 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'custom-preview-big.png'), |
408 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') | 408 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') |
409 | } | 409 | } |
410 | 410 | ||
@@ -615,7 +615,7 @@ describe('Test videos API validator', function () { | |||
615 | it('Should fail with a big thumbnail file', async function () { | 615 | it('Should fail with a big thumbnail file', async function () { |
616 | const fields = baseCorrectParams | 616 | const fields = baseCorrectParams |
617 | const attaches = { | 617 | const attaches = { |
618 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png') | 618 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'custom-preview-big.png') |
619 | } | 619 | } |
620 | 620 | ||
621 | await makeUploadRequest({ | 621 | await makeUploadRequest({ |
@@ -647,7 +647,7 @@ describe('Test videos API validator', function () { | |||
647 | it('Should fail with a big preview file', async function () { | 647 | it('Should fail with a big preview file', async function () { |
648 | const fields = baseCorrectParams | 648 | const fields = baseCorrectParams |
649 | const attaches = { | 649 | const attaches = { |
650 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png') | 650 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'custom-preview-big.png') |
651 | } | 651 | } |
652 | 652 | ||
653 | await makeUploadRequest({ | 653 | await makeUploadRequest({ |
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index 7ab67b126..2b302a8a2 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { basename, join } from 'path' | 4 | import { basename, join } from 'path' |
5 | import { SQLCommand, testImage, testLiveVideoResolutions } from '@server/tests/shared' | 5 | import { SQLCommand, testImageGeneratedByFFmpeg, testLiveVideoResolutions } from '@server/tests/shared' |
6 | import { getAllFiles, wait } from '@shared/core-utils' | 6 | import { getAllFiles, wait } from '@shared/core-utils' |
7 | import { ffprobePromise, getVideoStream } from '@shared/ffmpeg' | 7 | import { ffprobePromise, getVideoStream } from '@shared/ffmpeg' |
8 | import { | 8 | import { |
@@ -121,8 +121,8 @@ describe('Test live', function () { | |||
121 | expect(video.downloadEnabled).to.be.false | 121 | expect(video.downloadEnabled).to.be.false |
122 | expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC) | 122 | expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC) |
123 | 123 | ||
124 | await testImage(server.url, 'video_short1-preview.webm', video.previewPath) | 124 | await testImageGeneratedByFFmpeg(server.url, 'video_short1-preview.webm', video.previewPath) |
125 | await testImage(server.url, 'video_short1.webm', video.thumbnailPath) | 125 | await testImageGeneratedByFFmpeg(server.url, 'video_short1.webm', video.thumbnailPath) |
126 | 126 | ||
127 | const live = await server.live.get({ videoId: liveVideoUUID }) | 127 | const live = await server.live.get({ videoId: liveVideoUUID }) |
128 | 128 | ||
diff --git a/server/tests/api/object-storage/video-static-file-privacy.ts b/server/tests/api/object-storage/video-static-file-privacy.ts index af9d681b2..64ab542a5 100644 --- a/server/tests/api/object-storage/video-static-file-privacy.ts +++ b/server/tests/api/object-storage/video-static-file-privacy.ts | |||
@@ -39,7 +39,7 @@ describe('Object storage for video static file privacy', function () { | |||
39 | const video = await server.videos.getWithToken({ id: uuid }) | 39 | const video = await server.videos.getWithToken({ id: uuid }) |
40 | 40 | ||
41 | for (const file of video.files) { | 41 | for (const file of video.files) { |
42 | expectStartWith(file.fileUrl, server.url + '/object-storage-proxy/webseed/private/') | 42 | expectStartWith(file.fileUrl, server.url + '/object-storage-proxy/web-videos/private/') |
43 | 43 | ||
44 | await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | 44 | await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) |
45 | } | 45 | } |
@@ -107,15 +107,20 @@ describe('Object storage for video static file privacy', function () { | |||
107 | describe('VOD', function () { | 107 | describe('VOD', function () { |
108 | let privateVideoUUID: string | 108 | let privateVideoUUID: string |
109 | let publicVideoUUID: string | 109 | let publicVideoUUID: string |
110 | let passwordProtectedVideoUUID: string | ||
110 | let userPrivateVideoUUID: string | 111 | let userPrivateVideoUUID: string |
111 | 112 | ||
113 | const correctPassword = 'my super password' | ||
114 | const correctPasswordHeader = { 'x-peertube-video-password': correctPassword } | ||
115 | const incorrectPasswordHeader = { 'x-peertube-video-password': correctPassword + 'toto' } | ||
116 | |||
112 | // --------------------------------------------------------------------------- | 117 | // --------------------------------------------------------------------------- |
113 | 118 | ||
114 | async function getSampleFileUrls (videoId: string) { | 119 | async function getSampleFileUrls (videoId: string) { |
115 | const video = await server.videos.getWithToken({ id: videoId }) | 120 | const video = await server.videos.getWithToken({ id: videoId }) |
116 | 121 | ||
117 | return { | 122 | return { |
118 | webTorrentFile: video.files[0].fileUrl, | 123 | webVideoFile: video.files[0].fileUrl, |
119 | hlsFile: getHLS(video).files[0].fileUrl | 124 | hlsFile: getHLS(video).files[0].fileUrl |
120 | } | 125 | } |
121 | } | 126 | } |
@@ -140,6 +145,22 @@ describe('Object storage for video static file privacy', function () { | |||
140 | await checkPrivateVODFiles(privateVideoUUID) | 145 | await checkPrivateVODFiles(privateVideoUUID) |
141 | }) | 146 | }) |
142 | 147 | ||
148 | it('Should upload a password protected video and have appropriate object storage ACL', async function () { | ||
149 | this.timeout(120000) | ||
150 | |||
151 | { | ||
152 | const { uuid } = await server.videos.quickUpload({ | ||
153 | name: 'video', | ||
154 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
155 | videoPasswords: [ correctPassword ] | ||
156 | }) | ||
157 | passwordProtectedVideoUUID = uuid | ||
158 | } | ||
159 | await waitJobs([ server ]) | ||
160 | |||
161 | await checkPrivateVODFiles(passwordProtectedVideoUUID) | ||
162 | }) | ||
163 | |||
143 | it('Should upload a public video and have appropriate object storage ACL', async function () { | 164 | it('Should upload a public video and have appropriate object storage ACL', async function () { |
144 | this.timeout(120000) | 165 | this.timeout(120000) |
145 | 166 | ||
@@ -154,13 +175,49 @@ describe('Object storage for video static file privacy', function () { | |||
154 | it('Should not get files without appropriate OAuth token', async function () { | 175 | it('Should not get files without appropriate OAuth token', async function () { |
155 | this.timeout(60000) | 176 | this.timeout(60000) |
156 | 177 | ||
157 | const { webTorrentFile, hlsFile } = await getSampleFileUrls(privateVideoUUID) | 178 | const { webVideoFile, hlsFile } = await getSampleFileUrls(privateVideoUUID) |
179 | |||
180 | await makeRawRequest({ url: webVideoFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
181 | await makeRawRequest({ url: webVideoFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
182 | |||
183 | await makeRawRequest({ url: hlsFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
184 | await makeRawRequest({ url: hlsFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
185 | }) | ||
186 | |||
187 | it('Should not get files without appropriate password or appropriate OAuth token', async function () { | ||
188 | this.timeout(60000) | ||
189 | |||
190 | const { webVideoFile, hlsFile } = await getSampleFileUrls(passwordProtectedVideoUUID) | ||
158 | 191 | ||
159 | await makeRawRequest({ url: webTorrentFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 192 | await makeRawRequest({ url: webVideoFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
160 | await makeRawRequest({ url: webTorrentFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | 193 | await makeRawRequest({ |
194 | url: webVideoFile, | ||
195 | token: null, | ||
196 | headers: incorrectPasswordHeader, | ||
197 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
198 | }) | ||
199 | await makeRawRequest({ url: webVideoFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
200 | await makeRawRequest({ | ||
201 | url: webVideoFile, | ||
202 | token: null, | ||
203 | headers: correctPasswordHeader, | ||
204 | expectedStatus: HttpStatusCode.OK_200 | ||
205 | }) | ||
161 | 206 | ||
162 | await makeRawRequest({ url: hlsFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 207 | await makeRawRequest({ url: hlsFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
208 | await makeRawRequest({ | ||
209 | url: hlsFile, | ||
210 | token: null, | ||
211 | headers: incorrectPasswordHeader, | ||
212 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
213 | }) | ||
163 | await makeRawRequest({ url: hlsFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | 214 | await makeRawRequest({ url: hlsFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) |
215 | await makeRawRequest({ | ||
216 | url: hlsFile, | ||
217 | token: null, | ||
218 | headers: correctPasswordHeader, | ||
219 | expectedStatus: HttpStatusCode.OK_200 | ||
220 | }) | ||
164 | }) | 221 | }) |
165 | 222 | ||
166 | it('Should not get HLS file of another video', async function () { | 223 | it('Should not get HLS file of another video', async function () { |
@@ -176,21 +233,50 @@ describe('Object storage for video static file privacy', function () { | |||
176 | await makeRawRequest({ url: goodUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | 233 | await makeRawRequest({ url: goodUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) |
177 | }) | 234 | }) |
178 | 235 | ||
179 | it('Should correctly check OAuth or video file token', async function () { | 236 | it('Should correctly check OAuth, video file token of private video', async function () { |
180 | this.timeout(60000) | 237 | this.timeout(60000) |
181 | 238 | ||
182 | const badVideoFileToken = await server.videoToken.getVideoFileToken({ token: userToken, videoId: userPrivateVideoUUID }) | 239 | const badVideoFileToken = await server.videoToken.getVideoFileToken({ token: userToken, videoId: userPrivateVideoUUID }) |
183 | const goodVideoFileToken = await server.videoToken.getVideoFileToken({ videoId: privateVideoUUID }) | 240 | const goodVideoFileToken = await server.videoToken.getVideoFileToken({ videoId: privateVideoUUID }) |
184 | 241 | ||
185 | const { webTorrentFile, hlsFile } = await getSampleFileUrls(privateVideoUUID) | 242 | const { webVideoFile, hlsFile } = await getSampleFileUrls(privateVideoUUID) |
243 | |||
244 | for (const url of [ webVideoFile, hlsFile ]) { | ||
245 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
246 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
247 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
248 | |||
249 | await makeRawRequest({ url, query: { videoFileToken: badVideoFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
250 | await makeRawRequest({ url, query: { videoFileToken: goodVideoFileToken }, expectedStatus: HttpStatusCode.OK_200 }) | ||
251 | |||
252 | } | ||
253 | }) | ||
254 | |||
255 | it('Should correctly check OAuth, video file token or video password of password protected video', async function () { | ||
256 | this.timeout(60000) | ||
186 | 257 | ||
187 | for (const url of [ webTorrentFile, hlsFile ]) { | 258 | const badVideoFileToken = await server.videoToken.getVideoFileToken({ token: userToken, videoId: userPrivateVideoUUID }) |
259 | const goodVideoFileToken = await server.videoToken.getVideoFileToken({ | ||
260 | videoId: passwordProtectedVideoUUID, | ||
261 | videoPassword: correctPassword | ||
262 | }) | ||
263 | |||
264 | const { webVideoFile, hlsFile } = await getSampleFileUrls(passwordProtectedVideoUUID) | ||
265 | |||
266 | for (const url of [ hlsFile, webVideoFile ]) { | ||
188 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 267 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
189 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 268 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
190 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | 269 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) |
191 | 270 | ||
192 | await makeRawRequest({ url, query: { videoFileToken: badVideoFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 271 | await makeRawRequest({ url, query: { videoFileToken: badVideoFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
193 | await makeRawRequest({ url, query: { videoFileToken: goodVideoFileToken }, expectedStatus: HttpStatusCode.OK_200 }) | 272 | await makeRawRequest({ url, query: { videoFileToken: goodVideoFileToken }, expectedStatus: HttpStatusCode.OK_200 }) |
273 | |||
274 | await makeRawRequest({ | ||
275 | url, | ||
276 | headers: incorrectPasswordHeader, | ||
277 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
278 | }) | ||
279 | await makeRawRequest({ url, headers: correctPasswordHeader, expectedStatus: HttpStatusCode.OK_200 }) | ||
194 | } | 280 | } |
195 | }) | 281 | }) |
196 | 282 | ||
@@ -232,16 +318,26 @@ describe('Object storage for video static file privacy', function () { | |||
232 | let permanentLiveId: string | 318 | let permanentLiveId: string |
233 | let permanentLive: LiveVideo | 319 | let permanentLive: LiveVideo |
234 | 320 | ||
321 | let passwordProtectedLiveId: string | ||
322 | let passwordProtectedLive: LiveVideo | ||
323 | |||
324 | const correctPassword = 'my super password' | ||
325 | |||
235 | let unrelatedFileToken: string | 326 | let unrelatedFileToken: string |
236 | 327 | ||
237 | // --------------------------------------------------------------------------- | 328 | // --------------------------------------------------------------------------- |
238 | 329 | ||
239 | async function checkLiveFiles (live: LiveVideo, liveId: string) { | 330 | async function checkLiveFiles (live: LiveVideo, liveId: string, videoPassword?: string) { |
240 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | 331 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) |
241 | await server.live.waitUntilPublished({ videoId: liveId }) | 332 | await server.live.waitUntilPublished({ videoId: liveId }) |
242 | 333 | ||
243 | const video = await server.videos.getWithToken({ id: liveId }) | 334 | const video = videoPassword |
244 | const fileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) | 335 | ? await server.videos.getWithPassword({ id: liveId, password: videoPassword }) |
336 | : await server.videos.getWithToken({ id: liveId }) | ||
337 | |||
338 | const fileToken = videoPassword | ||
339 | ? await server.videoToken.getVideoFileToken({ token: null, videoId: video.uuid, videoPassword }) | ||
340 | : await server.videoToken.getVideoFileToken({ videoId: video.uuid }) | ||
245 | 341 | ||
246 | const hls = video.streamingPlaylists[0] | 342 | const hls = video.streamingPlaylists[0] |
247 | 343 | ||
@@ -253,10 +349,19 @@ describe('Object storage for video static file privacy', function () { | |||
253 | 349 | ||
254 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | 350 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) |
255 | await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) | 351 | await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) |
256 | 352 | if (videoPassword) { | |
353 | await makeRawRequest({ url, headers: { 'x-peertube-video-password': videoPassword }, expectedStatus: HttpStatusCode.OK_200 }) | ||
354 | } | ||
257 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 355 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
258 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 356 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
259 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 357 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
358 | if (videoPassword) { | ||
359 | await makeRawRequest({ | ||
360 | url, | ||
361 | headers: { 'x-peertube-video-password': 'incorrectPassword' }, | ||
362 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
363 | }) | ||
364 | } | ||
260 | } | 365 | } |
261 | 366 | ||
262 | await stopFfmpeg(ffmpegCommand) | 367 | await stopFfmpeg(ffmpegCommand) |
@@ -326,6 +431,17 @@ describe('Object storage for video static file privacy', function () { | |||
326 | permanentLiveId = video.uuid | 431 | permanentLiveId = video.uuid |
327 | permanentLive = live | 432 | permanentLive = live |
328 | } | 433 | } |
434 | |||
435 | { | ||
436 | const { video, live } = await server.live.quickCreate({ | ||
437 | saveReplay: false, | ||
438 | permanentLive: false, | ||
439 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
440 | videoPasswords: [ correctPassword ] | ||
441 | }) | ||
442 | passwordProtectedLiveId = video.uuid | ||
443 | passwordProtectedLive = live | ||
444 | } | ||
329 | }) | 445 | }) |
330 | 446 | ||
331 | it('Should create a private normal live and have a private static path', async function () { | 447 | it('Should create a private normal live and have a private static path', async function () { |
@@ -340,6 +456,12 @@ describe('Object storage for video static file privacy', function () { | |||
340 | await checkLiveFiles(permanentLive, permanentLiveId) | 456 | await checkLiveFiles(permanentLive, permanentLiveId) |
341 | }) | 457 | }) |
342 | 458 | ||
459 | it('Should create a password protected live and have a private static path', async function () { | ||
460 | this.timeout(240000) | ||
461 | |||
462 | await checkLiveFiles(passwordProtectedLive, passwordProtectedLiveId, correctPassword) | ||
463 | }) | ||
464 | |||
343 | it('Should reinject video file token in permanent live', async function () { | 465 | it('Should reinject video file token in permanent live', async function () { |
344 | this.timeout(240000) | 466 | this.timeout(240000) |
345 | 467 | ||
@@ -412,11 +534,11 @@ describe('Object storage for video static file privacy', function () { | |||
412 | 534 | ||
413 | it('Should not be able to access object storage proxy', async function () { | 535 | it('Should not be able to access object storage proxy', async function () { |
414 | const privateVideo = await server.videos.getWithToken({ id: videoUUID }) | 536 | const privateVideo = await server.videos.getWithToken({ id: videoUUID }) |
415 | const webtorrentFilename = extractFilenameFromUrl(privateVideo.files[0].fileUrl) | 537 | const webVideoFilename = extractFilenameFromUrl(privateVideo.files[0].fileUrl) |
416 | const hlsFilename = extractFilenameFromUrl(getHLS(privateVideo).files[0].fileUrl) | 538 | const hlsFilename = extractFilenameFromUrl(getHLS(privateVideo).files[0].fileUrl) |
417 | 539 | ||
418 | await makeRawRequest({ | 540 | await makeRawRequest({ |
419 | url: server.url + '/object-storage-proxy/webseed/private/' + webtorrentFilename, | 541 | url: server.url + '/object-storage-proxy/web-videos/private/' + webVideoFilename, |
420 | token: server.accessToken, | 542 | token: server.accessToken, |
421 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | 543 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 |
422 | }) | 544 | }) |
diff --git a/server/tests/api/object-storage/videos.ts b/server/tests/api/object-storage/videos.ts index f837d9966..dcc52ef06 100644 --- a/server/tests/api/object-storage/videos.ts +++ b/server/tests/api/object-storage/videos.ts | |||
@@ -41,8 +41,8 @@ async function checkFiles (options: { | |||
41 | playlistBucket: string | 41 | playlistBucket: string |
42 | playlistPrefix?: string | 42 | playlistPrefix?: string |
43 | 43 | ||
44 | webtorrentBucket: string | 44 | webVideoBucket: string |
45 | webtorrentPrefix?: string | 45 | webVideoPrefix?: string |
46 | }) { | 46 | }) { |
47 | const { | 47 | const { |
48 | server, | 48 | server, |
@@ -50,20 +50,20 @@ async function checkFiles (options: { | |||
50 | originSQLCommand, | 50 | originSQLCommand, |
51 | video, | 51 | video, |
52 | playlistBucket, | 52 | playlistBucket, |
53 | webtorrentBucket, | 53 | webVideoBucket, |
54 | baseMockUrl, | 54 | baseMockUrl, |
55 | playlistPrefix, | 55 | playlistPrefix, |
56 | webtorrentPrefix | 56 | webVideoPrefix |
57 | } = options | 57 | } = options |
58 | 58 | ||
59 | let allFiles = video.files | 59 | let allFiles = video.files |
60 | 60 | ||
61 | for (const file of video.files) { | 61 | for (const file of video.files) { |
62 | const baseUrl = baseMockUrl | 62 | const baseUrl = baseMockUrl |
63 | ? `${baseMockUrl}/${webtorrentBucket}/` | 63 | ? `${baseMockUrl}/${webVideoBucket}/` |
64 | : `http://${webtorrentBucket}.${ObjectStorageCommand.getMockEndpointHost()}/` | 64 | : `http://${webVideoBucket}.${ObjectStorageCommand.getMockEndpointHost()}/` |
65 | 65 | ||
66 | const prefix = webtorrentPrefix || '' | 66 | const prefix = webVideoPrefix || '' |
67 | const start = baseUrl + prefix | 67 | const start = baseUrl + prefix |
68 | 68 | ||
69 | expectStartWith(file.fileUrl, start) | 69 | expectStartWith(file.fileUrl, start) |
@@ -134,8 +134,8 @@ function runTestSuite (options: { | |||
134 | playlistBucket: string | 134 | playlistBucket: string |
135 | playlistPrefix?: string | 135 | playlistPrefix?: string |
136 | 136 | ||
137 | webtorrentBucket: string | 137 | webVideoBucket: string |
138 | webtorrentPrefix?: string | 138 | webVideoPrefix?: string |
139 | 139 | ||
140 | useMockBaseUrl?: boolean | 140 | useMockBaseUrl?: boolean |
141 | }) { | 141 | }) { |
@@ -161,7 +161,7 @@ function runTestSuite (options: { | |||
161 | : undefined | 161 | : undefined |
162 | 162 | ||
163 | await objectStorage.createMockBucket(options.playlistBucket) | 163 | await objectStorage.createMockBucket(options.playlistBucket) |
164 | await objectStorage.createMockBucket(options.webtorrentBucket) | 164 | await objectStorage.createMockBucket(options.webVideoBucket) |
165 | 165 | ||
166 | const config = { | 166 | const config = { |
167 | object_storage: { | 167 | object_storage: { |
@@ -181,11 +181,11 @@ function runTestSuite (options: { | |||
181 | : undefined | 181 | : undefined |
182 | }, | 182 | }, |
183 | 183 | ||
184 | videos: { | 184 | web_videos: { |
185 | bucket_name: options.webtorrentBucket, | 185 | bucket_name: options.webVideoBucket, |
186 | prefix: options.webtorrentPrefix, | 186 | prefix: options.webVideoPrefix, |
187 | base_url: baseMockUrl | 187 | base_url: baseMockUrl |
188 | ? `${baseMockUrl}/${options.webtorrentBucket}` | 188 | ? `${baseMockUrl}/${options.webVideoBucket}` |
189 | : undefined | 189 | : undefined |
190 | } | 190 | } |
191 | } | 191 | } |
@@ -308,7 +308,7 @@ describe('Object storage for videos', function () { | |||
308 | bucket_name: 'aaa' | 308 | bucket_name: 'aaa' |
309 | }, | 309 | }, |
310 | 310 | ||
311 | videos: { | 311 | web_videos: { |
312 | bucket_name: 'aaa' | 312 | bucket_name: 'aaa' |
313 | } | 313 | } |
314 | } | 314 | } |
@@ -386,27 +386,27 @@ describe('Object storage for videos', function () { | |||
386 | describe('Test simple object storage', function () { | 386 | describe('Test simple object storage', function () { |
387 | runTestSuite({ | 387 | runTestSuite({ |
388 | playlistBucket: objectStorage.getMockBucketName('streaming-playlists'), | 388 | playlistBucket: objectStorage.getMockBucketName('streaming-playlists'), |
389 | webtorrentBucket: objectStorage.getMockBucketName('videos') | 389 | webVideoBucket: objectStorage.getMockBucketName('web-videos') |
390 | }) | 390 | }) |
391 | }) | 391 | }) |
392 | 392 | ||
393 | describe('Test object storage with prefix', function () { | 393 | describe('Test object storage with prefix', function () { |
394 | runTestSuite({ | 394 | runTestSuite({ |
395 | playlistBucket: objectStorage.getMockBucketName('mybucket'), | 395 | playlistBucket: objectStorage.getMockBucketName('mybucket'), |
396 | webtorrentBucket: objectStorage.getMockBucketName('mybucket'), | 396 | webVideoBucket: objectStorage.getMockBucketName('mybucket'), |
397 | 397 | ||
398 | playlistPrefix: 'streaming-playlists_', | 398 | playlistPrefix: 'streaming-playlists_', |
399 | webtorrentPrefix: 'webtorrent_' | 399 | webVideoPrefix: 'webvideo_' |
400 | }) | 400 | }) |
401 | }) | 401 | }) |
402 | 402 | ||
403 | describe('Test object storage with prefix and base URL', function () { | 403 | describe('Test object storage with prefix and base URL', function () { |
404 | runTestSuite({ | 404 | runTestSuite({ |
405 | playlistBucket: objectStorage.getMockBucketName('mybucket'), | 405 | playlistBucket: objectStorage.getMockBucketName('mybucket'), |
406 | webtorrentBucket: objectStorage.getMockBucketName('mybucket'), | 406 | webVideoBucket: objectStorage.getMockBucketName('mybucket'), |
407 | 407 | ||
408 | playlistPrefix: 'streaming-playlists/', | 408 | playlistPrefix: 'streaming-playlists/', |
409 | webtorrentPrefix: 'webtorrent/', | 409 | webVideoPrefix: 'webvideo/', |
410 | 410 | ||
411 | useMockBaseUrl: true | 411 | useMockBaseUrl: true |
412 | }) | 412 | }) |
@@ -431,7 +431,7 @@ describe('Object storage for videos', function () { | |||
431 | runTestSuite({ | 431 | runTestSuite({ |
432 | maxUploadPart, | 432 | maxUploadPart, |
433 | playlistBucket: objectStorage.getMockBucketName('streaming-playlists'), | 433 | playlistBucket: objectStorage.getMockBucketName('streaming-playlists'), |
434 | webtorrentBucket: objectStorage.getMockBucketName('videos'), | 434 | webVideoBucket: objectStorage.getMockBucketName('web-videos'), |
435 | fixture | 435 | fixture |
436 | }) | 436 | }) |
437 | }) | 437 | }) |
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts index 5262c503f..0c5c27225 100644 --- a/server/tests/api/redundancy/redundancy.ts +++ b/server/tests/api/redundancy/redundancy.ts | |||
@@ -43,7 +43,7 @@ async function checkMagnetWebseeds (file: VideoFile, baseWebseeds: string[], ser | |||
43 | } | 43 | } |
44 | } | 44 | } |
45 | 45 | ||
46 | async function createServers (strategy: VideoRedundancyStrategy | null, additionalParams: any = {}, withWebtorrent = true) { | 46 | async function createServers (strategy: VideoRedundancyStrategy | null, additionalParams: any = {}, withWebVideo = true) { |
47 | const strategies: any[] = [] | 47 | const strategies: any[] = [] |
48 | 48 | ||
49 | if (strategy !== null) { | 49 | if (strategy !== null) { |
@@ -60,8 +60,8 @@ async function createServers (strategy: VideoRedundancyStrategy | null, addition | |||
60 | 60 | ||
61 | const config = { | 61 | const config = { |
62 | transcoding: { | 62 | transcoding: { |
63 | webtorrent: { | 63 | web_videos: { |
64 | enabled: withWebtorrent | 64 | enabled: withWebVideo |
65 | }, | 65 | }, |
66 | hls: { | 66 | hls: { |
67 | enabled: true | 67 | enabled: true |
@@ -100,7 +100,7 @@ async function createServers (strategy: VideoRedundancyStrategy | null, addition | |||
100 | } | 100 | } |
101 | 101 | ||
102 | async function ensureSameFilenames (videoUUID: string) { | 102 | async function ensureSameFilenames (videoUUID: string) { |
103 | let webtorrentFilenames: string[] | 103 | let webVideoFilenames: string[] |
104 | let hlsFilenames: string[] | 104 | let hlsFilenames: string[] |
105 | 105 | ||
106 | for (const server of servers) { | 106 | for (const server of servers) { |
@@ -108,24 +108,24 @@ async function ensureSameFilenames (videoUUID: string) { | |||
108 | 108 | ||
109 | // Ensure we use the same filenames that the origin | 109 | // Ensure we use the same filenames that the origin |
110 | 110 | ||
111 | const localWebtorrentFilenames = video.files.map(f => basename(f.fileUrl)).sort() | 111 | const localWebVideoFilenames = video.files.map(f => basename(f.fileUrl)).sort() |
112 | const localHLSFilenames = video.streamingPlaylists[0].files.map(f => basename(f.fileUrl)).sort() | 112 | const localHLSFilenames = video.streamingPlaylists[0].files.map(f => basename(f.fileUrl)).sort() |
113 | 113 | ||
114 | if (webtorrentFilenames) expect(webtorrentFilenames).to.deep.equal(localWebtorrentFilenames) | 114 | if (webVideoFilenames) expect(webVideoFilenames).to.deep.equal(localWebVideoFilenames) |
115 | else webtorrentFilenames = localWebtorrentFilenames | 115 | else webVideoFilenames = localWebVideoFilenames |
116 | 116 | ||
117 | if (hlsFilenames) expect(hlsFilenames).to.deep.equal(localHLSFilenames) | 117 | if (hlsFilenames) expect(hlsFilenames).to.deep.equal(localHLSFilenames) |
118 | else hlsFilenames = localHLSFilenames | 118 | else hlsFilenames = localHLSFilenames |
119 | } | 119 | } |
120 | 120 | ||
121 | return { webtorrentFilenames, hlsFilenames } | 121 | return { webVideoFilenames, hlsFilenames } |
122 | } | 122 | } |
123 | 123 | ||
124 | async function check1WebSeed (videoUUID?: string) { | 124 | async function check1WebSeed (videoUUID?: string) { |
125 | if (!videoUUID) videoUUID = video1Server2.uuid | 125 | if (!videoUUID) videoUUID = video1Server2.uuid |
126 | 126 | ||
127 | const webseeds = [ | 127 | const webseeds = [ |
128 | `${servers[1].url}/static/webseed/` | 128 | `${servers[1].url}/static/web-videos/` |
129 | ] | 129 | ] |
130 | 130 | ||
131 | for (const server of servers) { | 131 | for (const server of servers) { |
@@ -145,7 +145,7 @@ async function check2Webseeds (videoUUID?: string) { | |||
145 | 145 | ||
146 | const webseeds = [ | 146 | const webseeds = [ |
147 | `${servers[0].url}/static/redundancy/`, | 147 | `${servers[0].url}/static/redundancy/`, |
148 | `${servers[1].url}/static/webseed/` | 148 | `${servers[1].url}/static/web-videos/` |
149 | ] | 149 | ] |
150 | 150 | ||
151 | for (const server of servers) { | 151 | for (const server of servers) { |
@@ -156,11 +156,11 @@ async function check2Webseeds (videoUUID?: string) { | |||
156 | } | 156 | } |
157 | } | 157 | } |
158 | 158 | ||
159 | const { webtorrentFilenames } = await ensureSameFilenames(videoUUID) | 159 | const { webVideoFilenames } = await ensureSameFilenames(videoUUID) |
160 | 160 | ||
161 | const directories = [ | 161 | const directories = [ |
162 | servers[0].getDirectoryPath('redundancy'), | 162 | servers[0].getDirectoryPath('redundancy'), |
163 | servers[1].getDirectoryPath('videos') | 163 | servers[1].getDirectoryPath('web-videos') |
164 | ] | 164 | ] |
165 | 165 | ||
166 | for (const directory of directories) { | 166 | for (const directory of directories) { |
@@ -168,7 +168,7 @@ async function check2Webseeds (videoUUID?: string) { | |||
168 | expect(files).to.have.length.at.least(4) | 168 | expect(files).to.have.length.at.least(4) |
169 | 169 | ||
170 | // Ensure we files exist on disk | 170 | // Ensure we files exist on disk |
171 | expect(files.find(f => webtorrentFilenames.includes(f))).to.exist | 171 | expect(files.find(f => webVideoFilenames.includes(f))).to.exist |
172 | } | 172 | } |
173 | } | 173 | } |
174 | 174 | ||
diff --git a/server/tests/api/runners/runner-studio-transcoding.ts b/server/tests/api/runners/runner-studio-transcoding.ts index 41c556775..443a9d02a 100644 --- a/server/tests/api/runners/runner-studio-transcoding.ts +++ b/server/tests/api/runners/runner-studio-transcoding.ts | |||
@@ -104,7 +104,7 @@ describe('Test runner video studio transcoding', function () { | |||
104 | { | 104 | { |
105 | name: 'add-watermark' as 'add-watermark', | 105 | name: 'add-watermark' as 'add-watermark', |
106 | options: { | 106 | options: { |
107 | file: 'thumbnail.png' | 107 | file: 'custom-thumbnail.png' |
108 | } | 108 | } |
109 | }, | 109 | }, |
110 | { | 110 | { |
diff --git a/server/tests/api/runners/runner-vod-transcoding.ts b/server/tests/api/runners/runner-vod-transcoding.ts index d9da0f40d..ca16d9c10 100644 --- a/server/tests/api/runners/runner-vod-transcoding.ts +++ b/server/tests/api/runners/runner-vod-transcoding.ts | |||
@@ -424,7 +424,7 @@ describe('Test runner VOD transcoding', function () { | |||
424 | 424 | ||
425 | await servers[0].config.enableTranscoding(true, true) | 425 | await servers[0].config.enableTranscoding(true, true) |
426 | 426 | ||
427 | const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } | 427 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } |
428 | const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' }) | 428 | const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' }) |
429 | videoUUID = uuid | 429 | videoUUID = uuid |
430 | 430 | ||
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index 011ba268c..0e700eddb 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -46,6 +46,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) { | |||
46 | expect(data.cache.previews.size).to.equal(1) | 46 | expect(data.cache.previews.size).to.equal(1) |
47 | expect(data.cache.captions.size).to.equal(1) | 47 | expect(data.cache.captions.size).to.equal(1) |
48 | expect(data.cache.torrents.size).to.equal(1) | 48 | expect(data.cache.torrents.size).to.equal(1) |
49 | expect(data.cache.storyboards.size).to.equal(1) | ||
49 | 50 | ||
50 | expect(data.signup.enabled).to.be.true | 51 | expect(data.signup.enabled).to.be.true |
51 | expect(data.signup.limit).to.equal(4) | 52 | expect(data.signup.limit).to.equal(4) |
@@ -78,7 +79,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) { | |||
78 | expect(data.transcoding.resolutions['1440p']).to.be.true | 79 | expect(data.transcoding.resolutions['1440p']).to.be.true |
79 | expect(data.transcoding.resolutions['2160p']).to.be.true | 80 | expect(data.transcoding.resolutions['2160p']).to.be.true |
80 | expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.true | 81 | expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.true |
81 | expect(data.transcoding.webtorrent.enabled).to.be.true | 82 | expect(data.transcoding.webVideos.enabled).to.be.true |
82 | expect(data.transcoding.hls.enabled).to.be.true | 83 | expect(data.transcoding.hls.enabled).to.be.true |
83 | 84 | ||
84 | expect(data.live.enabled).to.be.false | 85 | expect(data.live.enabled).to.be.false |
@@ -154,6 +155,7 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
154 | expect(data.cache.previews.size).to.equal(2) | 155 | expect(data.cache.previews.size).to.equal(2) |
155 | expect(data.cache.captions.size).to.equal(3) | 156 | expect(data.cache.captions.size).to.equal(3) |
156 | expect(data.cache.torrents.size).to.equal(4) | 157 | expect(data.cache.torrents.size).to.equal(4) |
158 | expect(data.cache.storyboards.size).to.equal(5) | ||
157 | 159 | ||
158 | expect(data.signup.enabled).to.be.false | 160 | expect(data.signup.enabled).to.be.false |
159 | expect(data.signup.limit).to.equal(5) | 161 | expect(data.signup.limit).to.equal(5) |
@@ -190,7 +192,7 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
190 | expect(data.transcoding.resolutions['2160p']).to.be.false | 192 | expect(data.transcoding.resolutions['2160p']).to.be.false |
191 | expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.false | 193 | expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.false |
192 | expect(data.transcoding.hls.enabled).to.be.false | 194 | expect(data.transcoding.hls.enabled).to.be.false |
193 | expect(data.transcoding.webtorrent.enabled).to.be.true | 195 | expect(data.transcoding.webVideos.enabled).to.be.true |
194 | 196 | ||
195 | expect(data.live.enabled).to.be.true | 197 | expect(data.live.enabled).to.be.true |
196 | expect(data.live.allowReplay).to.be.true | 198 | expect(data.live.allowReplay).to.be.true |
@@ -290,6 +292,9 @@ const newCustomConfig: CustomConfig = { | |||
290 | }, | 292 | }, |
291 | torrents: { | 293 | torrents: { |
292 | size: 4 | 294 | size: 4 |
295 | }, | ||
296 | storyboards: { | ||
297 | size: 5 | ||
293 | } | 298 | } |
294 | }, | 299 | }, |
295 | signup: { | 300 | signup: { |
@@ -339,7 +344,7 @@ const newCustomConfig: CustomConfig = { | |||
339 | '2160p': false | 344 | '2160p': false |
340 | }, | 345 | }, |
341 | alwaysTranscodeOriginalResolution: false, | 346 | alwaysTranscodeOriginalResolution: false, |
342 | webtorrent: { | 347 | webVideos: { |
343 | enabled: true | 348 | enabled: true |
344 | }, | 349 | }, |
345 | hls: { | 350 | hls: { |
diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts index 2a5fff82b..e3e4605ee 100644 --- a/server/tests/api/server/follows.ts +++ b/server/tests/api/server/follows.ts | |||
@@ -6,611 +6,636 @@ import { Video, VideoPrivacy } from '@shared/models' | |||
6 | import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/server-commands' | 6 | import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/server-commands' |
7 | 7 | ||
8 | describe('Test follows', function () { | 8 | describe('Test follows', function () { |
9 | let servers: PeerTubeServer[] = [] | ||
10 | 9 | ||
11 | before(async function () { | 10 | describe('Complex follow', function () { |
12 | this.timeout(120000) | 11 | let servers: PeerTubeServer[] = [] |
13 | 12 | ||
14 | servers = await createMultipleServers(3) | 13 | before(async function () { |
14 | this.timeout(120000) | ||
15 | 15 | ||
16 | // Get the access tokens | 16 | servers = await createMultipleServers(3) |
17 | await setAccessTokensToServers(servers) | ||
18 | }) | ||
19 | 17 | ||
20 | describe('Data propagation after follow', function () { | 18 | // Get the access tokens |
19 | await setAccessTokensToServers(servers) | ||
20 | }) | ||
21 | 21 | ||
22 | it('Should not have followers/followings', async function () { | 22 | describe('Data propagation after follow', function () { |
23 | for (const server of servers) { | ||
24 | const bodies = await Promise.all([ | ||
25 | server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }), | ||
26 | server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' }) | ||
27 | ]) | ||
28 | 23 | ||
29 | for (const body of bodies) { | 24 | it('Should not have followers/followings', async function () { |
30 | expect(body.total).to.equal(0) | 25 | for (const server of servers) { |
26 | const bodies = await Promise.all([ | ||
27 | server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }), | ||
28 | server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' }) | ||
29 | ]) | ||
31 | 30 | ||
32 | const follows = body.data | 31 | for (const body of bodies) { |
33 | expect(follows).to.be.an('array') | 32 | expect(body.total).to.equal(0) |
34 | expect(follows).to.have.lengthOf(0) | 33 | |
34 | const follows = body.data | ||
35 | expect(follows).to.be.an('array') | ||
36 | expect(follows).to.have.lengthOf(0) | ||
37 | } | ||
35 | } | 38 | } |
36 | } | 39 | }) |
37 | }) | 40 | |
41 | it('Should have server 1 following root account of server 2 and server 3', async function () { | ||
42 | this.timeout(30000) | ||
38 | 43 | ||
39 | it('Should have server 1 following root account of server 2 and server 3', async function () { | 44 | await servers[0].follows.follow({ |
40 | this.timeout(30000) | 45 | hosts: [ servers[2].url ], |
46 | handles: [ 'root@' + servers[1].host ] | ||
47 | }) | ||
41 | 48 | ||
42 | await servers[0].follows.follow({ | 49 | await waitJobs(servers) |
43 | hosts: [ servers[2].url ], | ||
44 | handles: [ 'root@' + servers[1].host ] | ||
45 | }) | 50 | }) |
46 | 51 | ||
47 | await waitJobs(servers) | 52 | it('Should have 2 followings on server 1', async function () { |
48 | }) | 53 | const body = await servers[0].follows.getFollowings({ start: 0, count: 1, sort: 'createdAt' }) |
54 | expect(body.total).to.equal(2) | ||
49 | 55 | ||
50 | it('Should have 2 followings on server 1', async function () { | 56 | let follows = body.data |
51 | const body = await servers[0].follows.getFollowings({ start: 0, count: 1, sort: 'createdAt' }) | 57 | expect(follows).to.be.an('array') |
52 | expect(body.total).to.equal(2) | 58 | expect(follows).to.have.lengthOf(1) |
53 | 59 | ||
54 | let follows = body.data | 60 | const body2 = await servers[0].follows.getFollowings({ start: 1, count: 1, sort: 'createdAt' }) |
55 | expect(follows).to.be.an('array') | 61 | follows = follows.concat(body2.data) |
56 | expect(follows).to.have.lengthOf(1) | ||
57 | 62 | ||
58 | const body2 = await servers[0].follows.getFollowings({ start: 1, count: 1, sort: 'createdAt' }) | 63 | const server2Follow = follows.find(f => f.following.host === servers[1].host) |
59 | follows = follows.concat(body2.data) | 64 | const server3Follow = follows.find(f => f.following.host === servers[2].host) |
60 | 65 | ||
61 | const server2Follow = follows.find(f => f.following.host === servers[1].host) | 66 | expect(server2Follow).to.not.be.undefined |
62 | const server3Follow = follows.find(f => f.following.host === servers[2].host) | 67 | expect(server2Follow.following.name).to.equal('root') |
68 | expect(server2Follow.state).to.equal('accepted') | ||
63 | 69 | ||
64 | expect(server2Follow).to.not.be.undefined | 70 | expect(server3Follow).to.not.be.undefined |
65 | expect(server2Follow.following.name).to.equal('root') | 71 | expect(server3Follow.following.name).to.equal('peertube') |
66 | expect(server2Follow.state).to.equal('accepted') | 72 | expect(server3Follow.state).to.equal('accepted') |
73 | }) | ||
67 | 74 | ||
68 | expect(server3Follow).to.not.be.undefined | 75 | it('Should have 0 followings on server 2 and 3', async function () { |
69 | expect(server3Follow.following.name).to.equal('peertube') | 76 | for (const server of [ servers[1], servers[2] ]) { |
70 | expect(server3Follow.state).to.equal('accepted') | 77 | const body = await server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' }) |
71 | }) | 78 | expect(body.total).to.equal(0) |
72 | 79 | ||
73 | it('Should have 0 followings on server 2 and 3', async function () { | 80 | const follows = body.data |
74 | for (const server of [ servers[1], servers[2] ]) { | 81 | expect(follows).to.be.an('array') |
75 | const body = await server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' }) | 82 | expect(follows).to.have.lengthOf(0) |
76 | expect(body.total).to.equal(0) | 83 | } |
84 | }) | ||
85 | |||
86 | it('Should have 1 followers on server 3', async function () { | ||
87 | const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) | ||
88 | expect(body.total).to.equal(1) | ||
77 | 89 | ||
78 | const follows = body.data | 90 | const follows = body.data |
79 | expect(follows).to.be.an('array') | 91 | expect(follows).to.be.an('array') |
80 | expect(follows).to.have.lengthOf(0) | 92 | expect(follows).to.have.lengthOf(1) |
81 | } | 93 | expect(follows[0].follower.host).to.equal(servers[0].host) |
82 | }) | 94 | }) |
83 | 95 | ||
84 | it('Should have 1 followers on server 3', async function () { | 96 | it('Should have 0 followers on server 1 and 2', async function () { |
85 | const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) | 97 | for (const server of [ servers[0], servers[1] ]) { |
86 | expect(body.total).to.equal(1) | 98 | const body = await server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }) |
99 | expect(body.total).to.equal(0) | ||
87 | 100 | ||
88 | const follows = body.data | 101 | const follows = body.data |
89 | expect(follows).to.be.an('array') | 102 | expect(follows).to.be.an('array') |
90 | expect(follows).to.have.lengthOf(1) | 103 | expect(follows).to.have.lengthOf(0) |
91 | expect(follows[0].follower.host).to.equal(servers[0].host) | 104 | } |
92 | }) | 105 | }) |
93 | 106 | ||
94 | it('Should have 0 followers on server 1 and 2', async function () { | 107 | it('Should search/filter followings on server 1', async function () { |
95 | for (const server of [ servers[0], servers[1] ]) { | 108 | const sort = 'createdAt' |
96 | const body = await server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }) | 109 | const start = 0 |
97 | expect(body.total).to.equal(0) | 110 | const count = 1 |
98 | 111 | ||
99 | const follows = body.data | 112 | { |
100 | expect(follows).to.be.an('array') | 113 | const search = ':' + servers[1].port |
101 | expect(follows).to.have.lengthOf(0) | ||
102 | } | ||
103 | }) | ||
104 | 114 | ||
105 | it('Should search/filter followings on server 1', async function () { | 115 | { |
106 | const sort = 'createdAt' | 116 | const body = await servers[0].follows.getFollowings({ start, count, sort, search }) |
107 | const start = 0 | 117 | expect(body.total).to.equal(1) |
108 | const count = 1 | ||
109 | 118 | ||
110 | { | 119 | const follows = body.data |
111 | const search = ':' + servers[1].port | 120 | expect(follows).to.have.lengthOf(1) |
121 | expect(follows[0].following.host).to.equal(servers[1].host) | ||
122 | } | ||
112 | 123 | ||
113 | { | 124 | { |
114 | const body = await servers[0].follows.getFollowings({ start, count, sort, search }) | 125 | const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted' }) |
115 | expect(body.total).to.equal(1) | 126 | expect(body.total).to.equal(1) |
127 | expect(body.data).to.have.lengthOf(1) | ||
128 | } | ||
116 | 129 | ||
117 | const follows = body.data | 130 | { |
118 | expect(follows).to.have.lengthOf(1) | 131 | const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) |
119 | expect(follows[0].following.host).to.equal(servers[1].host) | 132 | expect(body.total).to.equal(1) |
120 | } | 133 | expect(body.data).to.have.lengthOf(1) |
134 | } | ||
121 | 135 | ||
122 | { | 136 | { |
123 | const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted' }) | 137 | const body = await servers[0].follows.getFollowings({ |
124 | expect(body.total).to.equal(1) | 138 | start, |
125 | expect(body.data).to.have.lengthOf(1) | 139 | count, |
140 | sort, | ||
141 | search, | ||
142 | state: 'accepted', | ||
143 | actorType: 'Application' | ||
144 | }) | ||
145 | expect(body.total).to.equal(0) | ||
146 | expect(body.data).to.have.lengthOf(0) | ||
147 | } | ||
148 | |||
149 | { | ||
150 | const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'pending' }) | ||
151 | expect(body.total).to.equal(0) | ||
152 | expect(body.data).to.have.lengthOf(0) | ||
153 | } | ||
126 | } | 154 | } |
127 | 155 | ||
128 | { | 156 | { |
129 | const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) | 157 | const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'root' }) |
130 | expect(body.total).to.equal(1) | 158 | expect(body.total).to.equal(1) |
131 | expect(body.data).to.have.lengthOf(1) | 159 | expect(body.data).to.have.lengthOf(1) |
132 | } | 160 | } |
133 | 161 | ||
134 | { | 162 | { |
135 | const body = await servers[0].follows.getFollowings({ | 163 | const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'bla' }) |
136 | start, | ||
137 | count, | ||
138 | sort, | ||
139 | search, | ||
140 | state: 'accepted', | ||
141 | actorType: 'Application' | ||
142 | }) | ||
143 | expect(body.total).to.equal(0) | 164 | expect(body.total).to.equal(0) |
144 | expect(body.data).to.have.lengthOf(0) | ||
145 | } | ||
146 | 165 | ||
147 | { | ||
148 | const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'pending' }) | ||
149 | expect(body.total).to.equal(0) | ||
150 | expect(body.data).to.have.lengthOf(0) | 166 | expect(body.data).to.have.lengthOf(0) |
151 | } | 167 | } |
152 | } | 168 | }) |
153 | 169 | ||
154 | { | 170 | it('Should search/filter followers on server 2', async function () { |
155 | const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'root' }) | 171 | const start = 0 |
156 | expect(body.total).to.equal(1) | 172 | const count = 5 |
157 | expect(body.data).to.have.lengthOf(1) | 173 | const sort = 'createdAt' |
158 | } | ||
159 | 174 | ||
160 | { | 175 | { |
161 | const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'bla' }) | 176 | const search = servers[0].port + '' |
162 | expect(body.total).to.equal(0) | ||
163 | 177 | ||
164 | expect(body.data).to.have.lengthOf(0) | 178 | { |
165 | } | 179 | const body = await servers[2].follows.getFollowers({ start, count, sort, search }) |
166 | }) | 180 | expect(body.total).to.equal(1) |
167 | 181 | ||
168 | it('Should search/filter followers on server 2', async function () { | 182 | const follows = body.data |
169 | const start = 0 | 183 | expect(follows).to.have.lengthOf(1) |
170 | const count = 5 | 184 | expect(follows[0].following.host).to.equal(servers[2].host) |
171 | const sort = 'createdAt' | 185 | } |
172 | 186 | ||
173 | { | 187 | { |
174 | const search = servers[0].port + '' | 188 | const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted' }) |
189 | expect(body.total).to.equal(1) | ||
190 | expect(body.data).to.have.lengthOf(1) | ||
191 | } | ||
175 | 192 | ||
176 | { | 193 | { |
177 | const body = await servers[2].follows.getFollowers({ start, count, sort, search }) | 194 | const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) |
178 | expect(body.total).to.equal(1) | 195 | expect(body.total).to.equal(0) |
196 | expect(body.data).to.have.lengthOf(0) | ||
197 | } | ||
179 | 198 | ||
180 | const follows = body.data | 199 | { |
181 | expect(follows).to.have.lengthOf(1) | 200 | const body = await servers[2].follows.getFollowers({ |
182 | expect(follows[0].following.host).to.equal(servers[2].host) | 201 | start, |
183 | } | 202 | count, |
203 | sort, | ||
204 | search, | ||
205 | state: 'accepted', | ||
206 | actorType: 'Application' | ||
207 | }) | ||
208 | expect(body.total).to.equal(1) | ||
209 | expect(body.data).to.have.lengthOf(1) | ||
210 | } | ||
184 | 211 | ||
185 | { | 212 | { |
186 | const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted' }) | 213 | const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'pending' }) |
187 | expect(body.total).to.equal(1) | 214 | expect(body.total).to.equal(0) |
188 | expect(body.data).to.have.lengthOf(1) | 215 | expect(body.data).to.have.lengthOf(0) |
216 | } | ||
189 | } | 217 | } |
190 | 218 | ||
191 | { | 219 | { |
192 | const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) | 220 | const body = await servers[2].follows.getFollowers({ start, count, sort, search: 'bla' }) |
193 | expect(body.total).to.equal(0) | 221 | expect(body.total).to.equal(0) |
194 | expect(body.data).to.have.lengthOf(0) | ||
195 | } | ||
196 | 222 | ||
197 | { | 223 | const follows = body.data |
198 | const body = await servers[2].follows.getFollowers({ | 224 | expect(follows).to.have.lengthOf(0) |
199 | start, | ||
200 | count, | ||
201 | sort, | ||
202 | search, | ||
203 | state: 'accepted', | ||
204 | actorType: 'Application' | ||
205 | }) | ||
206 | expect(body.total).to.equal(1) | ||
207 | expect(body.data).to.have.lengthOf(1) | ||
208 | } | 225 | } |
226 | }) | ||
209 | 227 | ||
210 | { | 228 | it('Should have the correct follows counts', async function () { |
211 | const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'pending' }) | 229 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) |
212 | expect(body.total).to.equal(0) | 230 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) |
213 | expect(body.data).to.have.lengthOf(0) | 231 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) |
214 | } | 232 | |
215 | } | 233 | // Server 2 and 3 does not know server 1 follow another server (there was not a refresh) |
234 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | ||
235 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) | ||
236 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) | ||
237 | |||
238 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | ||
239 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | ||
240 | }) | ||
241 | |||
242 | it('Should unfollow server 3 on server 1', async function () { | ||
243 | this.timeout(15000) | ||
244 | |||
245 | await servers[0].follows.unfollow({ target: servers[2] }) | ||
216 | 246 | ||
217 | { | 247 | await waitJobs(servers) |
218 | const body = await servers[2].follows.getFollowers({ start, count, sort, search: 'bla' }) | 248 | }) |
249 | |||
250 | it('Should not follow server 3 on server 1 anymore', async function () { | ||
251 | const body = await servers[0].follows.getFollowings({ start: 0, count: 2, sort: 'createdAt' }) | ||
252 | expect(body.total).to.equal(1) | ||
253 | |||
254 | const follows = body.data | ||
255 | expect(follows).to.be.an('array') | ||
256 | expect(follows).to.have.lengthOf(1) | ||
257 | |||
258 | expect(follows[0].following.host).to.equal(servers[1].host) | ||
259 | }) | ||
260 | |||
261 | it('Should not have server 1 as follower on server 3 anymore', async function () { | ||
262 | const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) | ||
219 | expect(body.total).to.equal(0) | 263 | expect(body.total).to.equal(0) |
220 | 264 | ||
221 | const follows = body.data | 265 | const follows = body.data |
266 | expect(follows).to.be.an('array') | ||
222 | expect(follows).to.have.lengthOf(0) | 267 | expect(follows).to.have.lengthOf(0) |
223 | } | 268 | }) |
224 | }) | ||
225 | 269 | ||
226 | it('Should have the correct follows counts', async function () { | 270 | it('Should have the correct follows counts after the unfollow', async function () { |
227 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) | 271 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) |
228 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) | 272 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) |
229 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | 273 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 }) |
230 | 274 | ||
231 | // Server 2 and 3 does not know server 1 follow another server (there was not a refresh) | 275 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) |
232 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | 276 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) |
233 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) | 277 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) |
234 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) | ||
235 | 278 | ||
236 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | 279 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 0 }) |
237 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | 280 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 }) |
238 | }) | 281 | }) |
239 | 282 | ||
240 | it('Should unfollow server 3 on server 1', async function () { | 283 | it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () { |
241 | this.timeout(15000) | 284 | this.timeout(160000) |
242 | 285 | ||
243 | await servers[0].follows.unfollow({ target: servers[2] }) | 286 | await servers[1].videos.upload({ attributes: { name: 'server2' } }) |
287 | await servers[2].videos.upload({ attributes: { name: 'server3' } }) | ||
244 | 288 | ||
245 | await waitJobs(servers) | 289 | await waitJobs(servers) |
246 | }) | ||
247 | 290 | ||
248 | it('Should not follow server 3 on server 1 anymore', async function () { | 291 | { |
249 | const body = await servers[0].follows.getFollowings({ start: 0, count: 2, sort: 'createdAt' }) | 292 | const { total, data } = await servers[0].videos.list() |
250 | expect(body.total).to.equal(1) | 293 | expect(total).to.equal(1) |
294 | expect(data[0].name).to.equal('server2') | ||
295 | } | ||
251 | 296 | ||
252 | const follows = body.data | 297 | { |
253 | expect(follows).to.be.an('array') | 298 | const { total, data } = await servers[1].videos.list() |
254 | expect(follows).to.have.lengthOf(1) | 299 | expect(total).to.equal(1) |
300 | expect(data[0].name).to.equal('server2') | ||
301 | } | ||
255 | 302 | ||
256 | expect(follows[0].following.host).to.equal(servers[1].host) | 303 | { |
257 | }) | 304 | const { total, data } = await servers[2].videos.list() |
305 | expect(total).to.equal(1) | ||
306 | expect(data[0].name).to.equal('server3') | ||
307 | } | ||
308 | }) | ||
258 | 309 | ||
259 | it('Should not have server 1 as follower on server 3 anymore', async function () { | 310 | it('Should remove account follow', async function () { |
260 | const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) | 311 | this.timeout(15000) |
261 | expect(body.total).to.equal(0) | ||
262 | 312 | ||
263 | const follows = body.data | 313 | await servers[0].follows.unfollow({ target: 'root@' + servers[1].host }) |
264 | expect(follows).to.be.an('array') | ||
265 | expect(follows).to.have.lengthOf(0) | ||
266 | }) | ||
267 | 314 | ||
268 | it('Should have the correct follows counts after the unfollow', async function () { | 315 | await waitJobs(servers) |
269 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | 316 | }) |
270 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) | ||
271 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 }) | ||
272 | 317 | ||
273 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | 318 | it('Should have removed the account follow', async function () { |
274 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) | 319 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) |
275 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) | 320 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) |
276 | 321 | ||
277 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 0 }) | 322 | { |
278 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 }) | 323 | const { total, data } = await servers[0].follows.getFollowings() |
279 | }) | 324 | expect(total).to.equal(0) |
325 | expect(data).to.have.lengthOf(0) | ||
326 | } | ||
280 | 327 | ||
281 | it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () { | 328 | { |
282 | this.timeout(160000) | 329 | const { total, data } = await servers[0].videos.list() |
330 | expect(total).to.equal(0) | ||
331 | expect(data).to.have.lengthOf(0) | ||
332 | } | ||
333 | }) | ||
283 | 334 | ||
284 | await servers[1].videos.upload({ attributes: { name: 'server2' } }) | 335 | it('Should follow a channel', async function () { |
285 | await servers[2].videos.upload({ attributes: { name: 'server3' } }) | 336 | this.timeout(15000) |
286 | 337 | ||
287 | await waitJobs(servers) | 338 | await servers[0].follows.follow({ |
339 | handles: [ 'root_channel@' + servers[1].host ] | ||
340 | }) | ||
288 | 341 | ||
289 | { | 342 | await waitJobs(servers) |
290 | const { total, data } = await servers[0].videos.list() | ||
291 | expect(total).to.equal(1) | ||
292 | expect(data[0].name).to.equal('server2') | ||
293 | } | ||
294 | 343 | ||
295 | { | 344 | await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) |
296 | const { total, data } = await servers[1].videos.list() | 345 | await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) |
297 | expect(total).to.equal(1) | ||
298 | expect(data[0].name).to.equal('server2') | ||
299 | } | ||
300 | 346 | ||
301 | { | 347 | { |
302 | const { total, data } = await servers[2].videos.list() | 348 | const { total, data } = await servers[0].follows.getFollowings() |
303 | expect(total).to.equal(1) | 349 | expect(total).to.equal(1) |
304 | expect(data[0].name).to.equal('server3') | 350 | expect(data).to.have.lengthOf(1) |
305 | } | 351 | } |
352 | |||
353 | { | ||
354 | const { total, data } = await servers[0].videos.list() | ||
355 | expect(total).to.equal(1) | ||
356 | expect(data).to.have.lengthOf(1) | ||
357 | } | ||
358 | }) | ||
306 | }) | 359 | }) |
307 | 360 | ||
308 | it('Should remove account follow', async function () { | 361 | describe('Should propagate data on a new server follow', function () { |
309 | this.timeout(15000) | 362 | let video4: Video |
310 | 363 | ||
311 | await servers[0].follows.unfollow({ target: 'root@' + servers[1].host }) | 364 | before(async function () { |
365 | this.timeout(240000) | ||
312 | 366 | ||
313 | await waitJobs(servers) | 367 | const video4Attributes = { |
314 | }) | 368 | name: 'server3-4', |
369 | category: 2, | ||
370 | nsfw: true, | ||
371 | licence: 6, | ||
372 | tags: [ 'tag1', 'tag2', 'tag3' ] | ||
373 | } | ||
315 | 374 | ||
316 | it('Should have removed the account follow', async function () { | 375 | await servers[2].videos.upload({ attributes: { name: 'server3-2' } }) |
317 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) | 376 | await servers[2].videos.upload({ attributes: { name: 'server3-3' } }) |
318 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) | ||
319 | 377 | ||
320 | { | 378 | const video4CreateResult = await servers[2].videos.upload({ attributes: video4Attributes }) |
321 | const { total, data } = await servers[0].follows.getFollowings() | ||
322 | expect(total).to.equal(0) | ||
323 | expect(data).to.have.lengthOf(0) | ||
324 | } | ||
325 | 379 | ||
326 | { | 380 | await servers[2].videos.upload({ attributes: { name: 'server3-5' } }) |
327 | const { total, data } = await servers[0].videos.list() | 381 | await servers[2].videos.upload({ attributes: { name: 'server3-6' } }) |
328 | expect(total).to.equal(0) | ||
329 | expect(data).to.have.lengthOf(0) | ||
330 | } | ||
331 | }) | ||
332 | 382 | ||
333 | it('Should follow a channel', async function () { | 383 | { |
334 | this.timeout(15000) | 384 | const userAccessToken = await servers[2].users.generateUserAndToken('captain') |
335 | 385 | ||
336 | await servers[0].follows.follow({ | 386 | await servers[2].videos.rate({ id: video4CreateResult.id, rating: 'like' }) |
337 | handles: [ 'root_channel@' + servers[1].host ] | 387 | await servers[2].videos.rate({ token: userAccessToken, id: video4CreateResult.id, rating: 'dislike' }) |
338 | }) | 388 | } |
339 | 389 | ||
340 | await waitJobs(servers) | 390 | { |
391 | await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'my super first comment' }) | ||
341 | 392 | ||
342 | await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) | 393 | await servers[2].comments.addReplyToLastThread({ text: 'my super answer to thread 1' }) |
343 | await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) | 394 | await servers[2].comments.addReplyToLastReply({ text: 'my super answer to answer of thread 1' }) |
395 | await servers[2].comments.addReplyToLastThread({ text: 'my second answer to thread 1' }) | ||
396 | } | ||
344 | 397 | ||
345 | { | 398 | { |
346 | const { total, data } = await servers[0].follows.getFollowings() | 399 | const { id: threadId } = await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'will be deleted' }) |
347 | expect(total).to.equal(1) | 400 | await servers[2].comments.addReplyToLastThread({ text: 'answer to deleted' }) |
348 | expect(data).to.have.lengthOf(1) | ||
349 | } | ||
350 | 401 | ||
351 | { | 402 | const { id: replyId } = await servers[2].comments.addReplyToLastThread({ text: 'will also be deleted' }) |
352 | const { total, data } = await servers[0].videos.list() | ||
353 | expect(total).to.equal(1) | ||
354 | expect(data).to.have.lengthOf(1) | ||
355 | } | ||
356 | }) | ||
357 | }) | ||
358 | 403 | ||
359 | describe('Should propagate data on a new server follow', function () { | 404 | await servers[2].comments.addReplyToLastReply({ text: 'my second answer to deleted' }) |
360 | let video4: Video | ||
361 | 405 | ||
362 | before(async function () { | 406 | await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: threadId }) |
363 | this.timeout(120000) | 407 | await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: replyId }) |
408 | } | ||
364 | 409 | ||
365 | const video4Attributes = { | 410 | await servers[2].captions.add({ |
366 | name: 'server3-4', | 411 | language: 'ar', |
367 | category: 2, | 412 | videoId: video4CreateResult.id, |
368 | nsfw: true, | 413 | fixture: 'subtitle-good2.vtt' |
369 | licence: 6, | 414 | }) |
370 | tags: [ 'tag1', 'tag2', 'tag3' ] | ||
371 | } | ||
372 | 415 | ||
373 | await servers[2].videos.upload({ attributes: { name: 'server3-2' } }) | 416 | await waitJobs(servers) |
374 | await servers[2].videos.upload({ attributes: { name: 'server3-3' } }) | ||
375 | const video4CreateResult = await servers[2].videos.upload({ attributes: video4Attributes }) | ||
376 | await servers[2].videos.upload({ attributes: { name: 'server3-5' } }) | ||
377 | await servers[2].videos.upload({ attributes: { name: 'server3-6' } }) | ||
378 | 417 | ||
379 | { | 418 | // Server 1 follows server 3 |
380 | const userAccessToken = await servers[2].users.generateUserAndToken('captain') | 419 | await servers[0].follows.follow({ hosts: [ servers[2].url ] }) |
381 | 420 | ||
382 | await servers[2].videos.rate({ id: video4CreateResult.id, rating: 'like' }) | 421 | await waitJobs(servers) |
383 | await servers[2].videos.rate({ token: userAccessToken, id: video4CreateResult.id, rating: 'dislike' }) | 422 | }) |
384 | } | ||
385 | 423 | ||
386 | { | 424 | it('Should have the correct follows counts', async function () { |
387 | await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'my super first comment' }) | 425 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) |
426 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) | ||
427 | await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) | ||
428 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | ||
388 | 429 | ||
389 | await servers[2].comments.addReplyToLastThread({ text: 'my super answer to thread 1' }) | 430 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) |
390 | await servers[2].comments.addReplyToLastReply({ text: 'my super answer to answer of thread 1' }) | 431 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) |
391 | await servers[2].comments.addReplyToLastThread({ text: 'my second answer to thread 1' }) | 432 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) |
392 | } | 433 | await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) |
393 | 434 | ||
394 | { | 435 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) |
395 | const { id: threadId } = await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'will be deleted' }) | 436 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) |
396 | await servers[2].comments.addReplyToLastThread({ text: 'answer to deleted' }) | 437 | }) |
397 | 438 | ||
398 | const { id: replyId } = await servers[2].comments.addReplyToLastThread({ text: 'will also be deleted' }) | 439 | it('Should have propagated videos', async function () { |
440 | const { total, data } = await servers[0].videos.list() | ||
441 | expect(total).to.equal(7) | ||
442 | |||
443 | const video2 = data.find(v => v.name === 'server3-2') | ||
444 | video4 = data.find(v => v.name === 'server3-4') | ||
445 | const video6 = data.find(v => v.name === 'server3-6') | ||
446 | |||
447 | expect(video2).to.not.be.undefined | ||
448 | expect(video4).to.not.be.undefined | ||
449 | expect(video6).to.not.be.undefined | ||
450 | |||
451 | const isLocal = false | ||
452 | const checkAttributes = { | ||
453 | name: 'server3-4', | ||
454 | category: 2, | ||
455 | licence: 6, | ||
456 | language: 'zh', | ||
457 | nsfw: true, | ||
458 | description: 'my super description', | ||
459 | support: 'my super support text', | ||
460 | account: { | ||
461 | name: 'root', | ||
462 | host: servers[2].host | ||
463 | }, | ||
464 | isLocal, | ||
465 | commentsEnabled: true, | ||
466 | downloadEnabled: true, | ||
467 | duration: 5, | ||
468 | tags: [ 'tag1', 'tag2', 'tag3' ], | ||
469 | privacy: VideoPrivacy.PUBLIC, | ||
470 | likes: 1, | ||
471 | dislikes: 1, | ||
472 | channel: { | ||
473 | displayName: 'Main root channel', | ||
474 | name: 'root_channel', | ||
475 | description: '', | ||
476 | isLocal | ||
477 | }, | ||
478 | fixture: 'video_short.webm', | ||
479 | files: [ | ||
480 | { | ||
481 | resolution: 720, | ||
482 | size: 218910 | ||
483 | } | ||
484 | ] | ||
485 | } | ||
486 | await completeVideoCheck({ | ||
487 | server: servers[0], | ||
488 | originServer: servers[2], | ||
489 | videoUUID: video4.uuid, | ||
490 | attributes: checkAttributes | ||
491 | }) | ||
492 | }) | ||
399 | 493 | ||
400 | await servers[2].comments.addReplyToLastReply({ text: 'my second answer to deleted' }) | 494 | it('Should have propagated comments', async function () { |
495 | const { total, data } = await servers[0].comments.listThreads({ videoId: video4.id, sort: 'createdAt' }) | ||
401 | 496 | ||
402 | await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: threadId }) | 497 | expect(total).to.equal(2) |
403 | await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: replyId }) | 498 | expect(data).to.be.an('array') |
404 | } | 499 | expect(data).to.have.lengthOf(2) |
405 | 500 | ||
406 | await servers[2].captions.add({ | 501 | { |
407 | language: 'ar', | 502 | const comment = data[0] |
408 | videoId: video4CreateResult.id, | 503 | expect(comment.inReplyToCommentId).to.be.null |
409 | fixture: 'subtitle-good2.vtt' | 504 | expect(comment.text).equal('my super first comment') |
410 | }) | 505 | expect(comment.videoId).to.equal(video4.id) |
506 | expect(comment.id).to.equal(comment.threadId) | ||
507 | expect(comment.account.name).to.equal('root') | ||
508 | expect(comment.account.host).to.equal(servers[2].host) | ||
509 | expect(comment.totalReplies).to.equal(3) | ||
510 | expect(dateIsValid(comment.createdAt as string)).to.be.true | ||
511 | expect(dateIsValid(comment.updatedAt as string)).to.be.true | ||
512 | |||
513 | const threadId = comment.threadId | ||
514 | |||
515 | const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId }) | ||
516 | expect(tree.comment.text).equal('my super first comment') | ||
517 | expect(tree.children).to.have.lengthOf(2) | ||
518 | |||
519 | const firstChild = tree.children[0] | ||
520 | expect(firstChild.comment.text).to.equal('my super answer to thread 1') | ||
521 | expect(firstChild.children).to.have.lengthOf(1) | ||
522 | |||
523 | const childOfFirstChild = firstChild.children[0] | ||
524 | expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1') | ||
525 | expect(childOfFirstChild.children).to.have.lengthOf(0) | ||
526 | |||
527 | const secondChild = tree.children[1] | ||
528 | expect(secondChild.comment.text).to.equal('my second answer to thread 1') | ||
529 | expect(secondChild.children).to.have.lengthOf(0) | ||
530 | } | ||
411 | 531 | ||
412 | await waitJobs(servers) | 532 | { |
533 | const deletedComment = data[1] | ||
534 | expect(deletedComment).to.not.be.undefined | ||
535 | expect(deletedComment.isDeleted).to.be.true | ||
536 | expect(deletedComment.deletedAt).to.not.be.null | ||
537 | expect(deletedComment.text).to.equal('') | ||
538 | expect(deletedComment.inReplyToCommentId).to.be.null | ||
539 | expect(deletedComment.account).to.be.null | ||
540 | expect(deletedComment.totalReplies).to.equal(2) | ||
541 | expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true | ||
542 | |||
543 | const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId: deletedComment.threadId }) | ||
544 | const [ commentRoot, deletedChildRoot ] = tree.children | ||
545 | |||
546 | expect(deletedChildRoot).to.not.be.undefined | ||
547 | expect(deletedChildRoot.comment.isDeleted).to.be.true | ||
548 | expect(deletedChildRoot.comment.deletedAt).to.not.be.null | ||
549 | expect(deletedChildRoot.comment.text).to.equal('') | ||
550 | expect(deletedChildRoot.comment.inReplyToCommentId).to.equal(deletedComment.id) | ||
551 | expect(deletedChildRoot.comment.account).to.be.null | ||
552 | expect(deletedChildRoot.children).to.have.lengthOf(1) | ||
553 | |||
554 | const answerToDeletedChild = deletedChildRoot.children[0] | ||
555 | expect(answerToDeletedChild.comment).to.not.be.undefined | ||
556 | expect(answerToDeletedChild.comment.inReplyToCommentId).to.equal(deletedChildRoot.comment.id) | ||
557 | expect(answerToDeletedChild.comment.text).to.equal('my second answer to deleted') | ||
558 | expect(answerToDeletedChild.comment.account.name).to.equal('root') | ||
559 | |||
560 | expect(commentRoot.comment).to.not.be.undefined | ||
561 | expect(commentRoot.comment.inReplyToCommentId).to.equal(deletedComment.id) | ||
562 | expect(commentRoot.comment.text).to.equal('answer to deleted') | ||
563 | expect(commentRoot.comment.account.name).to.equal('root') | ||
564 | } | ||
565 | }) | ||
413 | 566 | ||
414 | // Server 1 follows server 3 | 567 | it('Should have propagated captions', async function () { |
415 | await servers[0].follows.follow({ hosts: [ servers[2].url ] }) | 568 | const body = await servers[0].captions.list({ videoId: video4.id }) |
569 | expect(body.total).to.equal(1) | ||
570 | expect(body.data).to.have.lengthOf(1) | ||
416 | 571 | ||
417 | await waitJobs(servers) | 572 | const caption1 = body.data[0] |
418 | }) | 573 | expect(caption1.language.id).to.equal('ar') |
574 | expect(caption1.language.label).to.equal('Arabic') | ||
575 | expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/.+-ar.vtt$')) | ||
576 | await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.') | ||
577 | }) | ||
419 | 578 | ||
420 | it('Should have the correct follows counts', async function () { | 579 | it('Should unfollow server 3 on server 1 and does not list server 3 videos', async function () { |
421 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) | 580 | this.timeout(5000) |
422 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) | ||
423 | await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) | ||
424 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | ||
425 | 581 | ||
426 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | 582 | await servers[0].follows.unfollow({ target: servers[2] }) |
427 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) | ||
428 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) | ||
429 | await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) | ||
430 | 583 | ||
431 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | 584 | await waitJobs(servers) |
432 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | ||
433 | }) | ||
434 | 585 | ||
435 | it('Should have propagated videos', async function () { | 586 | const { total } = await servers[0].videos.list() |
436 | const { total, data } = await servers[0].videos.list() | 587 | expect(total).to.equal(1) |
437 | expect(total).to.equal(7) | ||
438 | |||
439 | const video2 = data.find(v => v.name === 'server3-2') | ||
440 | video4 = data.find(v => v.name === 'server3-4') | ||
441 | const video6 = data.find(v => v.name === 'server3-6') | ||
442 | |||
443 | expect(video2).to.not.be.undefined | ||
444 | expect(video4).to.not.be.undefined | ||
445 | expect(video6).to.not.be.undefined | ||
446 | |||
447 | const isLocal = false | ||
448 | const checkAttributes = { | ||
449 | name: 'server3-4', | ||
450 | category: 2, | ||
451 | licence: 6, | ||
452 | language: 'zh', | ||
453 | nsfw: true, | ||
454 | description: 'my super description', | ||
455 | support: 'my super support text', | ||
456 | account: { | ||
457 | name: 'root', | ||
458 | host: servers[2].host | ||
459 | }, | ||
460 | isLocal, | ||
461 | commentsEnabled: true, | ||
462 | downloadEnabled: true, | ||
463 | duration: 5, | ||
464 | tags: [ 'tag1', 'tag2', 'tag3' ], | ||
465 | privacy: VideoPrivacy.PUBLIC, | ||
466 | likes: 1, | ||
467 | dislikes: 1, | ||
468 | channel: { | ||
469 | displayName: 'Main root channel', | ||
470 | name: 'root_channel', | ||
471 | description: '', | ||
472 | isLocal | ||
473 | }, | ||
474 | fixture: 'video_short.webm', | ||
475 | files: [ | ||
476 | { | ||
477 | resolution: 720, | ||
478 | size: 218910 | ||
479 | } | ||
480 | ] | ||
481 | } | ||
482 | await completeVideoCheck({ | ||
483 | server: servers[0], | ||
484 | originServer: servers[2], | ||
485 | videoUUID: video4.uuid, | ||
486 | attributes: checkAttributes | ||
487 | }) | 588 | }) |
488 | }) | 589 | }) |
489 | 590 | ||
490 | it('Should have propagated comments', async function () { | 591 | after(async function () { |
491 | const { total, data } = await servers[0].comments.listThreads({ videoId: video4.id, sort: 'createdAt' }) | 592 | await cleanupTests(servers) |
492 | |||
493 | expect(total).to.equal(2) | ||
494 | expect(data).to.be.an('array') | ||
495 | expect(data).to.have.lengthOf(2) | ||
496 | |||
497 | { | ||
498 | const comment = data[0] | ||
499 | expect(comment.inReplyToCommentId).to.be.null | ||
500 | expect(comment.text).equal('my super first comment') | ||
501 | expect(comment.videoId).to.equal(video4.id) | ||
502 | expect(comment.id).to.equal(comment.threadId) | ||
503 | expect(comment.account.name).to.equal('root') | ||
504 | expect(comment.account.host).to.equal(servers[2].host) | ||
505 | expect(comment.totalReplies).to.equal(3) | ||
506 | expect(dateIsValid(comment.createdAt as string)).to.be.true | ||
507 | expect(dateIsValid(comment.updatedAt as string)).to.be.true | ||
508 | |||
509 | const threadId = comment.threadId | ||
510 | |||
511 | const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId }) | ||
512 | expect(tree.comment.text).equal('my super first comment') | ||
513 | expect(tree.children).to.have.lengthOf(2) | ||
514 | |||
515 | const firstChild = tree.children[0] | ||
516 | expect(firstChild.comment.text).to.equal('my super answer to thread 1') | ||
517 | expect(firstChild.children).to.have.lengthOf(1) | ||
518 | |||
519 | const childOfFirstChild = firstChild.children[0] | ||
520 | expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1') | ||
521 | expect(childOfFirstChild.children).to.have.lengthOf(0) | ||
522 | |||
523 | const secondChild = tree.children[1] | ||
524 | expect(secondChild.comment.text).to.equal('my second answer to thread 1') | ||
525 | expect(secondChild.children).to.have.lengthOf(0) | ||
526 | } | ||
527 | |||
528 | { | ||
529 | const deletedComment = data[1] | ||
530 | expect(deletedComment).to.not.be.undefined | ||
531 | expect(deletedComment.isDeleted).to.be.true | ||
532 | expect(deletedComment.deletedAt).to.not.be.null | ||
533 | expect(deletedComment.text).to.equal('') | ||
534 | expect(deletedComment.inReplyToCommentId).to.be.null | ||
535 | expect(deletedComment.account).to.be.null | ||
536 | expect(deletedComment.totalReplies).to.equal(2) | ||
537 | expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true | ||
538 | |||
539 | const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId: deletedComment.threadId }) | ||
540 | const [ commentRoot, deletedChildRoot ] = tree.children | ||
541 | |||
542 | expect(deletedChildRoot).to.not.be.undefined | ||
543 | expect(deletedChildRoot.comment.isDeleted).to.be.true | ||
544 | expect(deletedChildRoot.comment.deletedAt).to.not.be.null | ||
545 | expect(deletedChildRoot.comment.text).to.equal('') | ||
546 | expect(deletedChildRoot.comment.inReplyToCommentId).to.equal(deletedComment.id) | ||
547 | expect(deletedChildRoot.comment.account).to.be.null | ||
548 | expect(deletedChildRoot.children).to.have.lengthOf(1) | ||
549 | |||
550 | const answerToDeletedChild = deletedChildRoot.children[0] | ||
551 | expect(answerToDeletedChild.comment).to.not.be.undefined | ||
552 | expect(answerToDeletedChild.comment.inReplyToCommentId).to.equal(deletedChildRoot.comment.id) | ||
553 | expect(answerToDeletedChild.comment.text).to.equal('my second answer to deleted') | ||
554 | expect(answerToDeletedChild.comment.account.name).to.equal('root') | ||
555 | |||
556 | expect(commentRoot.comment).to.not.be.undefined | ||
557 | expect(commentRoot.comment.inReplyToCommentId).to.equal(deletedComment.id) | ||
558 | expect(commentRoot.comment.text).to.equal('answer to deleted') | ||
559 | expect(commentRoot.comment.account.name).to.equal('root') | ||
560 | } | ||
561 | }) | 593 | }) |
594 | }) | ||
562 | 595 | ||
563 | it('Should have propagated captions', async function () { | 596 | describe('Simple data propagation propagate data on a new channel follow', function () { |
564 | const body = await servers[0].captions.list({ videoId: video4.id }) | 597 | let servers: PeerTubeServer[] = [] |
565 | expect(body.total).to.equal(1) | ||
566 | expect(body.data).to.have.lengthOf(1) | ||
567 | 598 | ||
568 | const caption1 = body.data[0] | 599 | before(async function () { |
569 | expect(caption1.language.id).to.equal('ar') | 600 | this.timeout(120000) |
570 | expect(caption1.language.label).to.equal('Arabic') | ||
571 | expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/.+-ar.vtt$')) | ||
572 | await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.') | ||
573 | }) | ||
574 | 601 | ||
575 | it('Should unfollow server 3 on server 1 and does not list server 3 videos', async function () { | 602 | servers = await createMultipleServers(3) |
576 | this.timeout(5000) | 603 | await setAccessTokensToServers(servers) |
577 | 604 | ||
578 | await servers[0].follows.unfollow({ target: servers[2] }) | 605 | await servers[0].videos.upload({ attributes: { name: 'video to add' } }) |
579 | 606 | ||
580 | await waitJobs(servers) | 607 | await waitJobs(servers) |
581 | 608 | ||
582 | const { total } = await servers[0].videos.list() | 609 | for (const server of [ servers[1], servers[2] ]) { |
583 | expect(total).to.equal(1) | 610 | const video = await server.videos.find({ name: 'video to add' }) |
611 | expect(video).to.not.exist | ||
612 | } | ||
584 | }) | 613 | }) |
585 | }) | ||
586 | |||
587 | describe('Should propagate data on a new channel follow', function () { | ||
588 | 614 | ||
589 | before(async function () { | 615 | it('Should have propagated video after new channel follow', async function () { |
590 | this.timeout(60000) | 616 | this.timeout(60000) |
591 | 617 | ||
592 | await servers[2].videos.upload({ attributes: { name: 'server3-7' } }) | 618 | await servers[1].follows.follow({ handles: [ 'root_channel@' + servers[0].host ] }) |
593 | 619 | ||
594 | await waitJobs(servers) | 620 | await waitJobs(servers) |
595 | 621 | ||
596 | const video = await servers[0].videos.find({ name: 'server3-7' }) | 622 | const video = await servers[1].videos.find({ name: 'video to add' }) |
597 | expect(video).to.not.exist | 623 | expect(video).to.exist |
598 | }) | 624 | }) |
599 | 625 | ||
600 | it('Should have propagated channel video', async function () { | 626 | it('Should have propagated video after new account follow', async function () { |
601 | this.timeout(60000) | 627 | this.timeout(60000) |
602 | 628 | ||
603 | await servers[0].follows.follow({ handles: [ 'root_channel@' + servers[2].host ] }) | 629 | await servers[2].follows.follow({ handles: [ 'root@' + servers[0].host ] }) |
604 | 630 | ||
605 | await waitJobs(servers) | 631 | await waitJobs(servers) |
606 | 632 | ||
607 | const video = await servers[0].videos.find({ name: 'server3-7' }) | 633 | const video = await servers[2].videos.find({ name: 'video to add' }) |
608 | |||
609 | expect(video).to.exist | 634 | expect(video).to.exist |
610 | }) | 635 | }) |
611 | }) | ||
612 | 636 | ||
613 | after(async function () { | 637 | after(async function () { |
614 | await cleanupTests(servers) | 638 | await cleanupTests(servers) |
639 | }) | ||
615 | }) | 640 | }) |
616 | }) | 641 | }) |
diff --git a/server/tests/api/server/stats.ts b/server/tests/api/server/stats.ts index aad0d231a..a1bf189fa 100644 --- a/server/tests/api/server/stats.ts +++ b/server/tests/api/server/stats.ts | |||
@@ -194,7 +194,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
194 | newConfig: { | 194 | newConfig: { |
195 | transcoding: { | 195 | transcoding: { |
196 | enabled: true, | 196 | enabled: true, |
197 | webtorrent: { | 197 | webVideos: { |
198 | enabled: true | 198 | enabled: true |
199 | }, | 199 | }, |
200 | hls: { | 200 | hls: { |
diff --git a/server/tests/api/transcoding/audio-only.ts b/server/tests/api/transcoding/audio-only.ts index 1e31418e7..f4cc012ef 100644 --- a/server/tests/api/transcoding/audio-only.ts +++ b/server/tests/api/transcoding/audio-only.ts | |||
@@ -14,7 +14,7 @@ import { | |||
14 | describe('Test audio only video transcoding', function () { | 14 | describe('Test audio only video transcoding', function () { |
15 | let servers: PeerTubeServer[] = [] | 15 | let servers: PeerTubeServer[] = [] |
16 | let videoUUID: string | 16 | let videoUUID: string |
17 | let webtorrentAudioFileUrl: string | 17 | let webVideoAudioFileUrl: string |
18 | let fragmentedAudioFileUrl: string | 18 | let fragmentedAudioFileUrl: string |
19 | 19 | ||
20 | before(async function () { | 20 | before(async function () { |
@@ -37,7 +37,7 @@ describe('Test audio only video transcoding', function () { | |||
37 | hls: { | 37 | hls: { |
38 | enabled: true | 38 | enabled: true |
39 | }, | 39 | }, |
40 | webtorrent: { | 40 | web_videos: { |
41 | enabled: true | 41 | enabled: true |
42 | } | 42 | } |
43 | } | 43 | } |
@@ -71,7 +71,7 @@ describe('Test audio only video transcoding', function () { | |||
71 | } | 71 | } |
72 | 72 | ||
73 | if (server.serverNumber === 1) { | 73 | if (server.serverNumber === 1) { |
74 | webtorrentAudioFileUrl = video.files[2].fileUrl | 74 | webVideoAudioFileUrl = video.files[2].fileUrl |
75 | fragmentedAudioFileUrl = video.streamingPlaylists[0].files[2].fileUrl | 75 | fragmentedAudioFileUrl = video.streamingPlaylists[0].files[2].fileUrl |
76 | } | 76 | } |
77 | } | 77 | } |
@@ -79,7 +79,7 @@ describe('Test audio only video transcoding', function () { | |||
79 | 79 | ||
80 | it('0p transcoded video should not have video', async function () { | 80 | it('0p transcoded video should not have video', async function () { |
81 | const paths = [ | 81 | const paths = [ |
82 | servers[0].servers.buildWebTorrentFilePath(webtorrentAudioFileUrl), | 82 | servers[0].servers.buildWebVideoFilePath(webVideoAudioFileUrl), |
83 | servers[0].servers.buildFragmentedFilePath(videoUUID, fragmentedAudioFileUrl) | 83 | servers[0].servers.buildFragmentedFilePath(videoUUID, fragmentedAudioFileUrl) |
84 | ] | 84 | ] |
85 | 85 | ||
diff --git a/server/tests/api/transcoding/create-transcoding.ts b/server/tests/api/transcoding/create-transcoding.ts index d6f5b01dc..9a891043c 100644 --- a/server/tests/api/transcoding/create-transcoding.ts +++ b/server/tests/api/transcoding/create-transcoding.ts | |||
@@ -96,12 +96,12 @@ function runTests (enableObjectStorage: boolean) { | |||
96 | } | 96 | } |
97 | }) | 97 | }) |
98 | 98 | ||
99 | it('Should generate WebTorrent', async function () { | 99 | it('Should generate Web Video', async function () { |
100 | this.timeout(60000) | 100 | this.timeout(60000) |
101 | 101 | ||
102 | await servers[0].videos.runTranscoding({ | 102 | await servers[0].videos.runTranscoding({ |
103 | videoId: videoUUID, | 103 | videoId: videoUUID, |
104 | transcodingType: 'webtorrent' | 104 | transcodingType: 'web-video' |
105 | }) | 105 | }) |
106 | 106 | ||
107 | await waitJobs(servers) | 107 | await waitJobs(servers) |
@@ -117,13 +117,13 @@ function runTests (enableObjectStorage: boolean) { | |||
117 | } | 117 | } |
118 | }) | 118 | }) |
119 | 119 | ||
120 | it('Should generate WebTorrent from HLS only video', async function () { | 120 | it('Should generate Web Video from HLS only video', async function () { |
121 | this.timeout(60000) | 121 | this.timeout(60000) |
122 | 122 | ||
123 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: videoUUID }) | 123 | await servers[0].videos.removeAllWebVideoFiles({ videoId: videoUUID }) |
124 | await waitJobs(servers) | 124 | await waitJobs(servers) |
125 | 125 | ||
126 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'webtorrent' }) | 126 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'web-video' }) |
127 | await waitJobs(servers) | 127 | await waitJobs(servers) |
128 | 128 | ||
129 | for (const server of servers) { | 129 | for (const server of servers) { |
@@ -137,13 +137,13 @@ function runTests (enableObjectStorage: boolean) { | |||
137 | } | 137 | } |
138 | }) | 138 | }) |
139 | 139 | ||
140 | it('Should only generate WebTorrent', async function () { | 140 | it('Should only generate Web Video', async function () { |
141 | this.timeout(60000) | 141 | this.timeout(60000) |
142 | 142 | ||
143 | await servers[0].videos.removeHLSPlaylist({ videoId: videoUUID }) | 143 | await servers[0].videos.removeHLSPlaylist({ videoId: videoUUID }) |
144 | await waitJobs(servers) | 144 | await waitJobs(servers) |
145 | 145 | ||
146 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'webtorrent' }) | 146 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'web-video' }) |
147 | await waitJobs(servers) | 147 | await waitJobs(servers) |
148 | 148 | ||
149 | for (const server of servers) { | 149 | for (const server of servers) { |
@@ -165,7 +165,7 @@ function runTests (enableObjectStorage: boolean) { | |||
165 | enabled: true, | 165 | enabled: true, |
166 | resolutions: ConfigCommand.getCustomConfigResolutions(false), | 166 | resolutions: ConfigCommand.getCustomConfigResolutions(false), |
167 | 167 | ||
168 | webtorrent: { | 168 | webVideos: { |
169 | enabled: true | 169 | enabled: true |
170 | }, | 170 | }, |
171 | hls: { | 171 | hls: { |
@@ -201,7 +201,7 @@ function runTests (enableObjectStorage: boolean) { | |||
201 | enabled: true, | 201 | enabled: true, |
202 | resolutions: ConfigCommand.getCustomConfigResolutions(true), | 202 | resolutions: ConfigCommand.getCustomConfigResolutions(true), |
203 | 203 | ||
204 | webtorrent: { | 204 | webVideos: { |
205 | enabled: true | 205 | enabled: true |
206 | }, | 206 | }, |
207 | hls: { | 207 | hls: { |
diff --git a/server/tests/api/transcoding/hls.ts b/server/tests/api/transcoding/hls.ts index c668d7e0b..d67043c2a 100644 --- a/server/tests/api/transcoding/hls.ts +++ b/server/tests/api/transcoding/hls.ts | |||
@@ -75,8 +75,8 @@ describe('Test HLS videos', function () { | |||
75 | 75 | ||
76 | it('Should have the playlists/segment deleted from the disk', async function () { | 76 | it('Should have the playlists/segment deleted from the disk', async function () { |
77 | for (const server of servers) { | 77 | for (const server of servers) { |
78 | await checkDirectoryIsEmpty(server, 'videos', [ 'private' ]) | 78 | await checkDirectoryIsEmpty(server, 'web-videos', [ 'private' ]) |
79 | await checkDirectoryIsEmpty(server, join('videos', 'private')) | 79 | await checkDirectoryIsEmpty(server, join('web-videos', 'private')) |
80 | 80 | ||
81 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls'), [ 'private' ]) | 81 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls'), [ 'private' ]) |
82 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls', 'private')) | 82 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls', 'private')) |
@@ -111,7 +111,7 @@ describe('Test HLS videos', function () { | |||
111 | await doubleFollow(servers[0], servers[1]) | 111 | await doubleFollow(servers[0], servers[1]) |
112 | }) | 112 | }) |
113 | 113 | ||
114 | describe('With WebTorrent & HLS enabled', function () { | 114 | describe('With Web Video & HLS enabled', function () { |
115 | runTestSuite(false) | 115 | runTestSuite(false) |
116 | }) | 116 | }) |
117 | 117 | ||
@@ -136,7 +136,7 @@ describe('Test HLS videos', function () { | |||
136 | hls: { | 136 | hls: { |
137 | enabled: true | 137 | enabled: true |
138 | }, | 138 | }, |
139 | webtorrent: { | 139 | webVideos: { |
140 | enabled: false | 140 | enabled: false |
141 | } | 141 | } |
142 | } | 142 | } |
diff --git a/server/tests/api/transcoding/transcoder.ts b/server/tests/api/transcoding/transcoder.ts index 8a0a7f6d2..5386d236f 100644 --- a/server/tests/api/transcoding/transcoder.ts +++ b/server/tests/api/transcoding/transcoder.ts | |||
@@ -31,7 +31,7 @@ function updateConfigForTranscoding (server: PeerTubeServer) { | |||
31 | allowAdditionalExtensions: true, | 31 | allowAdditionalExtensions: true, |
32 | allowAudioFiles: true, | 32 | allowAudioFiles: true, |
33 | hls: { enabled: true }, | 33 | hls: { enabled: true }, |
34 | webtorrent: { enabled: true }, | 34 | webVideos: { enabled: true }, |
35 | resolutions: { | 35 | resolutions: { |
36 | '0p': false, | 36 | '0p': false, |
37 | '144p': true, | 37 | '144p': true, |
@@ -251,7 +251,7 @@ describe('Test video transcoding', function () { | |||
251 | expect(videoDetails.files).to.have.lengthOf(5) | 251 | expect(videoDetails.files).to.have.lengthOf(5) |
252 | 252 | ||
253 | const file = videoDetails.files.find(f => f.resolution.id === 240) | 253 | const file = videoDetails.files.find(f => f.resolution.id === 240) |
254 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 254 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
255 | const probe = await getAudioStream(path) | 255 | const probe = await getAudioStream(path) |
256 | 256 | ||
257 | if (probe.audioStream) { | 257 | if (probe.audioStream) { |
@@ -281,7 +281,7 @@ describe('Test video transcoding', function () { | |||
281 | const videoDetails = await server.videos.get({ id: video.id }) | 281 | const videoDetails = await server.videos.get({ id: video.id }) |
282 | 282 | ||
283 | const file = videoDetails.files.find(f => f.resolution.id === 240) | 283 | const file = videoDetails.files.find(f => f.resolution.id === 240) |
284 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 284 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
285 | 285 | ||
286 | expect(await hasAudioStream(path)).to.be.false | 286 | expect(await hasAudioStream(path)).to.be.false |
287 | } | 287 | } |
@@ -310,7 +310,7 @@ describe('Test video transcoding', function () { | |||
310 | const fixtureVideoProbe = await getAudioStream(fixturePath) | 310 | const fixtureVideoProbe = await getAudioStream(fixturePath) |
311 | 311 | ||
312 | const file = videoDetails.files.find(f => f.resolution.id === 240) | 312 | const file = videoDetails.files.find(f => f.resolution.id === 240) |
313 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 313 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
314 | 314 | ||
315 | const videoProbe = await getAudioStream(path) | 315 | const videoProbe = await getAudioStream(path) |
316 | 316 | ||
@@ -333,7 +333,7 @@ describe('Test video transcoding', function () { | |||
333 | newConfig: { | 333 | newConfig: { |
334 | transcoding: { | 334 | transcoding: { |
335 | hls: { enabled: true }, | 335 | hls: { enabled: true }, |
336 | webtorrent: { enabled: true }, | 336 | webVideos: { enabled: true }, |
337 | resolutions: { | 337 | resolutions: { |
338 | '0p': false, | 338 | '0p': false, |
339 | '144p': false, | 339 | '144p': false, |
@@ -353,7 +353,7 @@ describe('Test video transcoding', function () { | |||
353 | it('Should merge an audio file with the preview file', async function () { | 353 | it('Should merge an audio file with the preview file', async function () { |
354 | this.timeout(60_000) | 354 | this.timeout(60_000) |
355 | 355 | ||
356 | const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } | 356 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } |
357 | await servers[1].videos.upload({ attributes, mode }) | 357 | await servers[1].videos.upload({ attributes, mode }) |
358 | 358 | ||
359 | await waitJobs(servers) | 359 | await waitJobs(servers) |
@@ -405,7 +405,7 @@ describe('Test video transcoding', function () { | |||
405 | newConfig: { | 405 | newConfig: { |
406 | transcoding: { | 406 | transcoding: { |
407 | hls: { enabled: true }, | 407 | hls: { enabled: true }, |
408 | webtorrent: { enabled: true }, | 408 | webVideos: { enabled: true }, |
409 | resolutions: { | 409 | resolutions: { |
410 | '0p': true, | 410 | '0p': true, |
411 | '144p': false, | 411 | '144p': false, |
@@ -416,7 +416,7 @@ describe('Test video transcoding', function () { | |||
416 | } | 416 | } |
417 | }) | 417 | }) |
418 | 418 | ||
419 | const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } | 419 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } |
420 | const { id } = await servers[1].videos.upload({ attributes, mode }) | 420 | const { id } = await servers[1].videos.upload({ attributes, mode }) |
421 | 421 | ||
422 | await waitJobs(servers) | 422 | await waitJobs(servers) |
@@ -472,14 +472,14 @@ describe('Test video transcoding', function () { | |||
472 | 472 | ||
473 | for (const resolution of [ 144, 240, 360, 480 ]) { | 473 | for (const resolution of [ 144, 240, 360, 480 ]) { |
474 | const file = videoDetails.files.find(f => f.resolution.id === resolution) | 474 | const file = videoDetails.files.find(f => f.resolution.id === resolution) |
475 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 475 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
476 | const fps = await getVideoStreamFPS(path) | 476 | const fps = await getVideoStreamFPS(path) |
477 | 477 | ||
478 | expect(fps).to.be.below(31) | 478 | expect(fps).to.be.below(31) |
479 | } | 479 | } |
480 | 480 | ||
481 | const file = videoDetails.files.find(f => f.resolution.id === 720) | 481 | const file = videoDetails.files.find(f => f.resolution.id === 720) |
482 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 482 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
483 | const fps = await getVideoStreamFPS(path) | 483 | const fps = await getVideoStreamFPS(path) |
484 | 484 | ||
485 | expect(fps).to.be.above(58).and.below(62) | 485 | expect(fps).to.be.above(58).and.below(62) |
@@ -516,14 +516,14 @@ describe('Test video transcoding', function () { | |||
516 | 516 | ||
517 | { | 517 | { |
518 | const file = video.files.find(f => f.resolution.id === 240) | 518 | const file = video.files.find(f => f.resolution.id === 240) |
519 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 519 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
520 | const fps = await getVideoStreamFPS(path) | 520 | const fps = await getVideoStreamFPS(path) |
521 | expect(fps).to.be.equal(25) | 521 | expect(fps).to.be.equal(25) |
522 | } | 522 | } |
523 | 523 | ||
524 | { | 524 | { |
525 | const file = video.files.find(f => f.resolution.id === 720) | 525 | const file = video.files.find(f => f.resolution.id === 720) |
526 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 526 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
527 | const fps = await getVideoStreamFPS(path) | 527 | const fps = await getVideoStreamFPS(path) |
528 | expect(fps).to.be.equal(59) | 528 | expect(fps).to.be.equal(59) |
529 | } | 529 | } |
@@ -556,7 +556,7 @@ describe('Test video transcoding', function () { | |||
556 | 556 | ||
557 | for (const resolution of [ 240, 360, 480, 720, 1080 ]) { | 557 | for (const resolution of [ 240, 360, 480, 720, 1080 ]) { |
558 | const file = video.files.find(f => f.resolution.id === resolution) | 558 | const file = video.files.find(f => f.resolution.id === resolution) |
559 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 559 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
560 | 560 | ||
561 | const bitrate = await getVideoStreamBitrate(path) | 561 | const bitrate = await getVideoStreamBitrate(path) |
562 | const fps = await getVideoStreamFPS(path) | 562 | const fps = await getVideoStreamFPS(path) |
@@ -586,7 +586,7 @@ describe('Test video transcoding', function () { | |||
586 | '1440p': true, | 586 | '1440p': true, |
587 | '2160p': true | 587 | '2160p': true |
588 | }, | 588 | }, |
589 | webtorrent: { enabled: true }, | 589 | webVideos: { enabled: true }, |
590 | hls: { enabled: true } | 590 | hls: { enabled: true } |
591 | } | 591 | } |
592 | } | 592 | } |
@@ -607,7 +607,7 @@ describe('Test video transcoding', function () { | |||
607 | for (const r of resolutions) { | 607 | for (const r of resolutions) { |
608 | const file = video.files.find(f => f.resolution.id === r) | 608 | const file = video.files.find(f => f.resolution.id === r) |
609 | 609 | ||
610 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 610 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
611 | const bitrate = await getVideoStreamBitrate(path) | 611 | const bitrate = await getVideoStreamBitrate(path) |
612 | 612 | ||
613 | const inputBitrate = 60_000 | 613 | const inputBitrate = 60_000 |
@@ -631,7 +631,7 @@ describe('Test video transcoding', function () { | |||
631 | { | 631 | { |
632 | const video = await servers[1].videos.get({ id: videoUUID }) | 632 | const video = await servers[1].videos.get({ id: videoUUID }) |
633 | const file = video.files.find(f => f.resolution.id === 240) | 633 | const file = video.files.find(f => f.resolution.id === 240) |
634 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 634 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
635 | 635 | ||
636 | const probe = await ffprobePromise(path) | 636 | const probe = await ffprobePromise(path) |
637 | const metadata = new VideoFileMetadata(probe) | 637 | const metadata = new VideoFileMetadata(probe) |
@@ -704,14 +704,14 @@ describe('Test video transcoding', function () { | |||
704 | expect(transcodingJobs).to.have.lengthOf(16) | 704 | expect(transcodingJobs).to.have.lengthOf(16) |
705 | 705 | ||
706 | const hlsJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-hls') | 706 | const hlsJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-hls') |
707 | const webtorrentJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-webtorrent') | 707 | const webVideoJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-web-video') |
708 | const optimizeJobs = transcodingJobs.filter(j => j.data.type === 'optimize-to-webtorrent') | 708 | const optimizeJobs = transcodingJobs.filter(j => j.data.type === 'optimize-to-web-video') |
709 | 709 | ||
710 | expect(hlsJobs).to.have.lengthOf(8) | 710 | expect(hlsJobs).to.have.lengthOf(8) |
711 | expect(webtorrentJobs).to.have.lengthOf(7) | 711 | expect(webVideoJobs).to.have.lengthOf(7) |
712 | expect(optimizeJobs).to.have.lengthOf(1) | 712 | expect(optimizeJobs).to.have.lengthOf(1) |
713 | 713 | ||
714 | for (const j of optimizeJobs.concat(hlsJobs.concat(webtorrentJobs))) { | 714 | for (const j of optimizeJobs.concat(hlsJobs.concat(webVideoJobs))) { |
715 | expect(j.priority).to.be.greaterThan(100) | 715 | expect(j.priority).to.be.greaterThan(100) |
716 | expect(j.priority).to.be.lessThan(150) | 716 | expect(j.priority).to.be.lessThan(150) |
717 | } | 717 | } |
@@ -728,7 +728,7 @@ describe('Test video transcoding', function () { | |||
728 | transcoding: { | 728 | transcoding: { |
729 | enabled: true, | 729 | enabled: true, |
730 | hls: { enabled: true }, | 730 | hls: { enabled: true }, |
731 | webtorrent: { enabled: true }, | 731 | webVideos: { enabled: true }, |
732 | resolutions: { | 732 | resolutions: { |
733 | '0p': false, | 733 | '0p': false, |
734 | '144p': false, | 734 | '144p': false, |
diff --git a/server/tests/api/transcoding/update-while-transcoding.ts b/server/tests/api/transcoding/update-while-transcoding.ts index 61655f102..cfb4fa0cc 100644 --- a/server/tests/api/transcoding/update-while-transcoding.ts +++ b/server/tests/api/transcoding/update-while-transcoding.ts | |||
@@ -96,7 +96,7 @@ describe('Test update video privacy while transcoding', function () { | |||
96 | await doubleFollow(servers[0], servers[1]) | 96 | await doubleFollow(servers[0], servers[1]) |
97 | }) | 97 | }) |
98 | 98 | ||
99 | describe('With WebTorrent & HLS enabled', function () { | 99 | describe('With Web Video & HLS enabled', function () { |
100 | runTestSuite(false) | 100 | runTestSuite(false) |
101 | }) | 101 | }) |
102 | 102 | ||
@@ -121,7 +121,7 @@ describe('Test update video privacy while transcoding', function () { | |||
121 | hls: { | 121 | hls: { |
122 | enabled: true | 122 | enabled: true |
123 | }, | 123 | }, |
124 | webtorrent: { | 124 | webVideos: { |
125 | enabled: false | 125 | enabled: false |
126 | } | 126 | } |
127 | } | 127 | } |
diff --git a/server/tests/api/transcoding/video-studio.ts b/server/tests/api/transcoding/video-studio.ts index d1298caf7..ba68f8e24 100644 --- a/server/tests/api/transcoding/video-studio.ts +++ b/server/tests/api/transcoding/video-studio.ts | |||
@@ -241,7 +241,7 @@ describe('Test video studio', function () { | |||
241 | { | 241 | { |
242 | name: 'add-watermark', | 242 | name: 'add-watermark', |
243 | options: { | 243 | options: { |
244 | file: 'thumbnail.png' | 244 | file: 'custom-thumbnail.png' |
245 | } | 245 | } |
246 | } | 246 | } |
247 | ]) | 247 | ]) |
@@ -273,11 +273,11 @@ describe('Test video studio', function () { | |||
273 | describe('HLS only studio edition', function () { | 273 | describe('HLS only studio edition', function () { |
274 | 274 | ||
275 | before(async function () { | 275 | before(async function () { |
276 | // Disable webtorrent | 276 | // Disable Web Videos |
277 | await servers[0].config.updateExistingSubConfig({ | 277 | await servers[0].config.updateExistingSubConfig({ |
278 | newConfig: { | 278 | newConfig: { |
279 | transcoding: { | 279 | transcoding: { |
280 | webtorrent: { | 280 | webVideos: { |
281 | enabled: false | 281 | enabled: false |
282 | } | 282 | } |
283 | } | 283 | } |
@@ -354,8 +354,8 @@ describe('Test video studio', function () { | |||
354 | expect(oldFileUrls).to.not.include(f.fileUrl) | 354 | expect(oldFileUrls).to.not.include(f.fileUrl) |
355 | } | 355 | } |
356 | 356 | ||
357 | for (const webtorrentFile of video.files) { | 357 | for (const webVideoFile of video.files) { |
358 | expectStartWith(webtorrentFile.fileUrl, objectStorage.getMockWebVideosBaseUrl()) | 358 | expectStartWith(webVideoFile.fileUrl, objectStorage.getMockWebVideosBaseUrl()) |
359 | } | 359 | } |
360 | 360 | ||
361 | for (const hlsFile of video.streamingPlaylists[0].files) { | 361 | for (const hlsFile of video.streamingPlaylists[0].files) { |
diff --git a/server/tests/api/users/user-videos.ts b/server/tests/api/users/user-videos.ts index 696949504..77226e48e 100644 --- a/server/tests/api/users/user-videos.ts +++ b/server/tests/api/users/user-videos.ts | |||
@@ -184,12 +184,12 @@ describe('Test user videos', function () { | |||
184 | } | 184 | } |
185 | }) | 185 | }) |
186 | 186 | ||
187 | it('Should disable webtorrent, enable HLS, and update my quota', async function () { | 187 | it('Should disable web videos, enable HLS, and update my quota', async function () { |
188 | this.timeout(160000) | 188 | this.timeout(160000) |
189 | 189 | ||
190 | { | 190 | { |
191 | const config = await server.config.getCustomConfig() | 191 | const config = await server.config.getCustomConfig() |
192 | config.transcoding.webtorrent.enabled = false | 192 | config.transcoding.webVideos.enabled = false |
193 | config.transcoding.hls.enabled = true | 193 | config.transcoding.hls.enabled = true |
194 | config.transcoding.enabled = true | 194 | config.transcoding.enabled = true |
195 | await server.config.updateCustomSubConfig({ newConfig: config }) | 195 | await server.config.updateCustomSubConfig({ newConfig: config }) |
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 1c00f9a93..67ade1d0d 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -229,25 +229,13 @@ describe('Test users', function () { | |||
229 | }) | 229 | }) |
230 | 230 | ||
231 | it('Should be able to change the p2p attribute', async function () { | 231 | it('Should be able to change the p2p attribute', async function () { |
232 | { | 232 | await server.users.updateMe({ |
233 | await server.users.updateMe({ | 233 | token: userToken, |
234 | token: userToken, | 234 | p2pEnabled: true |
235 | webTorrentEnabled: false | 235 | }) |
236 | }) | ||
237 | |||
238 | const user = await server.users.getMyInfo({ token: userToken }) | ||
239 | expect(user.p2pEnabled).to.be.false | ||
240 | } | ||
241 | |||
242 | { | ||
243 | await server.users.updateMe({ | ||
244 | token: userToken, | ||
245 | p2pEnabled: true | ||
246 | }) | ||
247 | 236 | ||
248 | const user = await server.users.getMyInfo({ token: userToken }) | 237 | const user = await server.users.getMyInfo({ token: userToken }) |
249 | expect(user.p2pEnabled).to.be.true | 238 | expect(user.p2pEnabled).to.be.true |
250 | } | ||
251 | }) | 239 | }) |
252 | 240 | ||
253 | it('Should be able to change the email attribute', async function () { | 241 | it('Should be able to change the email attribute', async function () { |
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts index 357c08199..9c79b3aa6 100644 --- a/server/tests/api/videos/index.ts +++ b/server/tests/api/videos/index.ts | |||
@@ -20,3 +20,4 @@ import './videos-history' | |||
20 | import './videos-overview' | 20 | import './videos-overview' |
21 | import './video-source' | 21 | import './video-source' |
22 | import './video-static-file-privacy' | 22 | import './video-static-file-privacy' |
23 | import './video-storyboard' | ||
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index 27ba00d3d..e9aa0e3a1 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts | |||
@@ -9,7 +9,7 @@ import { | |||
9 | completeVideoCheck, | 9 | completeVideoCheck, |
10 | dateIsValid, | 10 | dateIsValid, |
11 | saveVideoInServers, | 11 | saveVideoInServers, |
12 | testImage | 12 | testImageGeneratedByFFmpeg |
13 | } from '@server/tests/shared' | 13 | } from '@server/tests/shared' |
14 | import { buildAbsoluteFixturePath, wait } from '@shared/core-utils' | 14 | import { buildAbsoluteFixturePath, wait } from '@shared/core-utils' |
15 | import { HttpStatusCode, VideoCommentThreadTree, VideoPrivacy } from '@shared/models' | 15 | import { HttpStatusCode, VideoCommentThreadTree, VideoPrivacy } from '@shared/models' |
@@ -70,8 +70,9 @@ describe('Test multiple servers', function () { | |||
70 | }) | 70 | }) |
71 | 71 | ||
72 | describe('Should upload the video and propagate on each server', function () { | 72 | describe('Should upload the video and propagate on each server', function () { |
73 | |||
73 | it('Should upload the video on server 1 and propagate on each server', async function () { | 74 | it('Should upload the video on server 1 and propagate on each server', async function () { |
74 | this.timeout(25000) | 75 | this.timeout(60000) |
75 | 76 | ||
76 | const attributes = { | 77 | const attributes = { |
77 | name: 'my super name for server 1', | 78 | name: 'my super name for server 1', |
@@ -175,8 +176,8 @@ describe('Test multiple servers', function () { | |||
175 | support: 'my super support text for server 2', | 176 | support: 'my super support text for server 2', |
176 | tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ], | 177 | tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ], |
177 | fixture: 'video_short2.webm', | 178 | fixture: 'video_short2.webm', |
178 | thumbnailfile: 'thumbnail.jpg', | 179 | thumbnailfile: 'custom-thumbnail.jpg', |
179 | previewfile: 'preview.jpg' | 180 | previewfile: 'custom-preview.jpg' |
180 | } | 181 | } |
181 | await servers[1].videos.upload({ token: userAccessToken, attributes, mode: 'resumable' }) | 182 | await servers[1].videos.upload({ token: userAccessToken, attributes, mode: 'resumable' }) |
182 | 183 | ||
@@ -229,8 +230,8 @@ describe('Test multiple servers', function () { | |||
229 | size: 750000 | 230 | size: 750000 |
230 | } | 231 | } |
231 | ], | 232 | ], |
232 | thumbnailfile: 'thumbnail', | 233 | thumbnailfile: 'custom-thumbnail', |
233 | previewfile: 'preview' | 234 | previewfile: 'custom-preview' |
234 | } | 235 | } |
235 | 236 | ||
236 | const { data } = await server.videos.list() | 237 | const { data } = await server.videos.list() |
@@ -619,9 +620,9 @@ describe('Test multiple servers', function () { | |||
619 | description: 'my super description updated', | 620 | description: 'my super description updated', |
620 | support: 'my super support text updated', | 621 | support: 'my super support text updated', |
621 | tags: [ 'tag_up_1', 'tag_up_2' ], | 622 | tags: [ 'tag_up_1', 'tag_up_2' ], |
622 | thumbnailfile: 'thumbnail.jpg', | 623 | thumbnailfile: 'custom-thumbnail.jpg', |
623 | originallyPublishedAt: '2019-02-11T13:38:14.449Z', | 624 | originallyPublishedAt: '2019-02-11T13:38:14.449Z', |
624 | previewfile: 'preview.jpg' | 625 | previewfile: 'custom-preview.jpg' |
625 | } | 626 | } |
626 | 627 | ||
627 | updatedAtMin = new Date() | 628 | updatedAtMin = new Date() |
@@ -674,8 +675,8 @@ describe('Test multiple servers', function () { | |||
674 | size: 292677 | 675 | size: 292677 |
675 | } | 676 | } |
676 | ], | 677 | ], |
677 | thumbnailfile: 'thumbnail', | 678 | thumbnailfile: 'custom-thumbnail', |
678 | previewfile: 'preview' | 679 | previewfile: 'custom-preview' |
679 | } | 680 | } |
680 | await completeVideoCheck({ server, originServer: servers[2], videoUUID: videoUpdated.uuid, attributes: checkAttributes }) | 681 | await completeVideoCheck({ server, originServer: servers[2], videoUUID: videoUpdated.uuid, attributes: checkAttributes }) |
681 | } | 682 | } |
@@ -685,7 +686,7 @@ describe('Test multiple servers', function () { | |||
685 | this.timeout(30000) | 686 | this.timeout(30000) |
686 | 687 | ||
687 | const attributes = { | 688 | const attributes = { |
688 | thumbnailfile: 'thumbnail.jpg' | 689 | thumbnailfile: 'custom-thumbnail.jpg' |
689 | } | 690 | } |
690 | 691 | ||
691 | updatedAtMin = new Date() | 692 | updatedAtMin = new Date() |
@@ -761,7 +762,7 @@ describe('Test multiple servers', function () { | |||
761 | for (const server of servers) { | 762 | for (const server of servers) { |
762 | const video = await server.videos.get({ id: videoUUID }) | 763 | const video = await server.videos.get({ id: videoUUID }) |
763 | 764 | ||
764 | await testImage(server.url, 'video_short1-preview.webm', video.previewPath) | 765 | await testImageGeneratedByFFmpeg(server.url, 'video_short1-preview.webm', video.previewPath) |
765 | } | 766 | } |
766 | }) | 767 | }) |
767 | }) | 768 | }) |
diff --git a/server/tests/api/videos/resumable-upload.ts b/server/tests/api/videos/resumable-upload.ts index 2fbefb392..91eb61833 100644 --- a/server/tests/api/videos/resumable-upload.ts +++ b/server/tests/api/videos/resumable-upload.ts | |||
@@ -93,10 +93,10 @@ describe('Test resumable upload', function () { | |||
93 | expect((await stat(filePath)).size).to.equal(expectedSize) | 93 | expect((await stat(filePath)).size).to.equal(expectedSize) |
94 | } | 94 | } |
95 | 95 | ||
96 | async function countResumableUploads () { | 96 | async function countResumableUploads (wait?: number) { |
97 | const subPath = join('tmp', 'resumable-uploads') | 97 | const subPath = join('tmp', 'resumable-uploads') |
98 | const filePath = server.servers.buildDirectory(subPath) | 98 | const filePath = server.servers.buildDirectory(subPath) |
99 | 99 | await new Promise(resolve => setTimeout(resolve, wait)) | |
100 | const files = await readdir(filePath) | 100 | const files = await readdir(filePath) |
101 | return files.length | 101 | return files.length |
102 | } | 102 | } |
@@ -122,14 +122,20 @@ describe('Test resumable upload', function () { | |||
122 | 122 | ||
123 | describe('Directory cleaning', function () { | 123 | describe('Directory cleaning', function () { |
124 | 124 | ||
125 | // FIXME: https://github.com/kukhariev/node-uploadx/pull/524/files#r852989382 | 125 | it('Should correctly delete files after an upload', async function () { |
126 | // it('Should correctly delete files after an upload', async function () { | 126 | const uploadId = await prepareUpload() |
127 | // const uploadId = await prepareUpload() | 127 | await sendChunks({ pathUploadId: uploadId }) |
128 | // await sendChunks({ pathUploadId: uploadId }) | 128 | await server.videos.endResumableUpload({ pathUploadId: uploadId }) |
129 | // await server.videos.endResumableUpload({ pathUploadId: uploadId }) | 129 | |
130 | expect(await countResumableUploads()).to.equal(0) | ||
131 | }) | ||
132 | |||
133 | it('Should correctly delete corrupt files', async function () { | ||
134 | const uploadId = await prepareUpload({ size: 8 * 1024 }) | ||
135 | await sendChunks({ pathUploadId: uploadId, size: 8 * 1024, expectedStatus: HttpStatusCode.UNPROCESSABLE_ENTITY_422 }) | ||
130 | 136 | ||
131 | // expect(await countResumableUploads()).to.equal(0) | 137 | expect(await countResumableUploads(2000)).to.equal(0) |
132 | // }) | 138 | }) |
133 | 139 | ||
134 | it('Should not delete files after an unfinished upload', async function () { | 140 | it('Should not delete files after an unfinished upload', async function () { |
135 | await prepareUpload() | 141 | await prepareUpload() |
@@ -254,6 +260,24 @@ describe('Test resumable upload', function () { | |||
254 | expect(result2.headers['x-resumable-upload-cached']).to.not.exist | 260 | expect(result2.headers['x-resumable-upload-cached']).to.not.exist |
255 | }) | 261 | }) |
256 | 262 | ||
263 | it('Should not cache after video deletion', async function () { | ||
264 | const originalName = 'toto.mp4' | ||
265 | const lastModified = new Date().getTime() | ||
266 | |||
267 | const uploadId1 = await prepareUpload({ originalName, lastModified }) | ||
268 | const result1 = await sendChunks({ pathUploadId: uploadId1 }) | ||
269 | await server.videos.remove({ id: result1.body.video.uuid }) | ||
270 | |||
271 | const uploadId2 = await prepareUpload({ originalName, lastModified }) | ||
272 | const result2 = await sendChunks({ pathUploadId: uploadId2 }) | ||
273 | expect(result1.body.video.uuid).to.not.equal(result2.body.video.uuid) | ||
274 | |||
275 | expect(result2.headers['x-resumable-upload-cached']).to.not.exist | ||
276 | |||
277 | await checkFileSize(uploadId1, null) | ||
278 | await checkFileSize(uploadId2, null) | ||
279 | }) | ||
280 | |||
257 | it('Should refuse an invalid digest', async function () { | 281 | it('Should refuse an invalid digest', async function () { |
258 | const uploadId = await prepareUpload({ token: server.accessToken }) | 282 | const uploadId = await prepareUpload({ token: server.accessToken }) |
259 | 283 | ||
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts index 0cb64d5a5..66414aa5b 100644 --- a/server/tests/api/videos/single-server.ts +++ b/server/tests/api/videos/single-server.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { checkVideoFilesWereRemoved, completeVideoCheck, testImage } from '@server/tests/shared' | 4 | import { checkVideoFilesWereRemoved, completeVideoCheck, testImageGeneratedByFFmpeg } from '@server/tests/shared' |
5 | import { wait } from '@shared/core-utils' | 5 | import { wait } from '@shared/core-utils' |
6 | import { Video, VideoPrivacy } from '@shared/models' | 6 | import { Video, VideoPrivacy } from '@shared/models' |
7 | import { | 7 | import { |
@@ -260,7 +260,7 @@ describe('Test a single server', function () { | |||
260 | 260 | ||
261 | for (const video of data) { | 261 | for (const video of data) { |
262 | const videoName = video.name.replace(' name', '') | 262 | const videoName = video.name.replace(' name', '') |
263 | await testImage(server.url, videoName, video.thumbnailPath) | 263 | await testImageGeneratedByFFmpeg(server.url, videoName, video.thumbnailPath) |
264 | } | 264 | } |
265 | }) | 265 | }) |
266 | 266 | ||
diff --git a/server/tests/api/videos/video-files.ts b/server/tests/api/videos/video-files.ts index 8c913bf31..0a183c44d 100644 --- a/server/tests/api/videos/video-files.ts +++ b/server/tests/api/videos/video-files.ts | |||
@@ -48,10 +48,10 @@ describe('Test videos files', function () { | |||
48 | await waitJobs(servers) | 48 | await waitJobs(servers) |
49 | }) | 49 | }) |
50 | 50 | ||
51 | it('Should delete webtorrent files', async function () { | 51 | it('Should delete web video files', async function () { |
52 | this.timeout(30_000) | 52 | this.timeout(30_000) |
53 | 53 | ||
54 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1 }) | 54 | await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1 }) |
55 | 55 | ||
56 | await waitJobs(servers) | 56 | await waitJobs(servers) |
57 | 57 | ||
@@ -80,15 +80,15 @@ describe('Test videos files', function () { | |||
80 | }) | 80 | }) |
81 | 81 | ||
82 | describe('When deleting a specific file', function () { | 82 | describe('When deleting a specific file', function () { |
83 | let webtorrentId: string | 83 | let webVideoId: string |
84 | let hlsId: string | 84 | let hlsId: string |
85 | 85 | ||
86 | before(async function () { | 86 | before(async function () { |
87 | this.timeout(120_000) | 87 | this.timeout(120_000) |
88 | 88 | ||
89 | { | 89 | { |
90 | const { uuid } = await servers[0].videos.quickUpload({ name: 'webtorrent' }) | 90 | const { uuid } = await servers[0].videos.quickUpload({ name: 'web-video' }) |
91 | webtorrentId = uuid | 91 | webVideoId = uuid |
92 | } | 92 | } |
93 | 93 | ||
94 | { | 94 | { |
@@ -99,38 +99,38 @@ describe('Test videos files', function () { | |||
99 | await waitJobs(servers) | 99 | await waitJobs(servers) |
100 | }) | 100 | }) |
101 | 101 | ||
102 | it('Shoulde delete a webtorrent file', async function () { | 102 | it('Shoulde delete a web video file', async function () { |
103 | this.timeout(30_000) | 103 | this.timeout(30_000) |
104 | 104 | ||
105 | const video = await servers[0].videos.get({ id: webtorrentId }) | 105 | const video = await servers[0].videos.get({ id: webVideoId }) |
106 | const files = video.files | 106 | const files = video.files |
107 | 107 | ||
108 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentId, fileId: files[0].id }) | 108 | await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: files[0].id }) |
109 | 109 | ||
110 | await waitJobs(servers) | 110 | await waitJobs(servers) |
111 | 111 | ||
112 | for (const server of servers) { | 112 | for (const server of servers) { |
113 | const video = await server.videos.get({ id: webtorrentId }) | 113 | const video = await server.videos.get({ id: webVideoId }) |
114 | 114 | ||
115 | expect(video.files).to.have.lengthOf(files.length - 1) | 115 | expect(video.files).to.have.lengthOf(files.length - 1) |
116 | expect(video.files.find(f => f.id === files[0].id)).to.not.exist | 116 | expect(video.files.find(f => f.id === files[0].id)).to.not.exist |
117 | } | 117 | } |
118 | }) | 118 | }) |
119 | 119 | ||
120 | it('Should delete all webtorrent files', async function () { | 120 | it('Should delete all web video files', async function () { |
121 | this.timeout(30_000) | 121 | this.timeout(30_000) |
122 | 122 | ||
123 | const video = await servers[0].videos.get({ id: webtorrentId }) | 123 | const video = await servers[0].videos.get({ id: webVideoId }) |
124 | const files = video.files | 124 | const files = video.files |
125 | 125 | ||
126 | for (const file of files) { | 126 | for (const file of files) { |
127 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentId, fileId: file.id }) | 127 | await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: file.id }) |
128 | } | 128 | } |
129 | 129 | ||
130 | await waitJobs(servers) | 130 | await waitJobs(servers) |
131 | 131 | ||
132 | for (const server of servers) { | 132 | for (const server of servers) { |
133 | const video = await server.videos.get({ id: webtorrentId }) | 133 | const video = await server.videos.get({ id: webVideoId }) |
134 | 134 | ||
135 | expect(video.files).to.have.lengthOf(0) | 135 | expect(video.files).to.have.lengthOf(0) |
136 | } | 136 | } |
@@ -182,16 +182,16 @@ describe('Test videos files', function () { | |||
182 | it('Should not delete last file of a video', async function () { | 182 | it('Should not delete last file of a video', async function () { |
183 | this.timeout(60_000) | 183 | this.timeout(60_000) |
184 | 184 | ||
185 | const webtorrentOnly = await servers[0].videos.get({ id: hlsId }) | 185 | const webVideoOnly = await servers[0].videos.get({ id: hlsId }) |
186 | const hlsOnly = await servers[0].videos.get({ id: webtorrentId }) | 186 | const hlsOnly = await servers[0].videos.get({ id: webVideoId }) |
187 | 187 | ||
188 | for (let i = 0; i < 4; i++) { | 188 | for (let i = 0; i < 4; i++) { |
189 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentOnly.id, fileId: webtorrentOnly.files[i].id }) | 189 | await servers[0].videos.removeWebVideoFile({ videoId: webVideoOnly.id, fileId: webVideoOnly.files[i].id }) |
190 | await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[i].id }) | 190 | await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[i].id }) |
191 | } | 191 | } |
192 | 192 | ||
193 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | 193 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 |
194 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentOnly.id, fileId: webtorrentOnly.files[4].id, expectedStatus }) | 194 | await servers[0].videos.removeWebVideoFile({ videoId: webVideoOnly.id, fileId: webVideoOnly.files[4].id, expectedStatus }) |
195 | await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[4].id, expectedStatus }) | 195 | await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[4].id, expectedStatus }) |
196 | }) | 196 | }) |
197 | }) | 197 | }) |
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts index 192b2aeb9..b78b4f344 100644 --- a/server/tests/api/videos/video-imports.ts +++ b/server/tests/api/videos/video-imports.ts | |||
@@ -3,7 +3,7 @@ | |||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { pathExists, readdir, remove } from 'fs-extra' | 4 | import { pathExists, readdir, remove } from 'fs-extra' |
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import { FIXTURE_URLS, testCaptionFile, testImage } from '@server/tests/shared' | 6 | import { FIXTURE_URLS, testCaptionFile, testImageGeneratedByFFmpeg } from '@server/tests/shared' |
7 | import { areHttpImportTestsDisabled } from '@shared/core-utils' | 7 | import { areHttpImportTestsDisabled } from '@shared/core-utils' |
8 | import { CustomConfig, HttpStatusCode, Video, VideoImportState, VideoPrivacy, VideoResolution, VideoState } from '@shared/models' | 8 | import { CustomConfig, HttpStatusCode, Video, VideoImportState, VideoPrivacy, VideoResolution, VideoState } from '@shared/models' |
9 | import { | 9 | import { |
@@ -67,7 +67,7 @@ async function checkVideoServer2 (server: PeerTubeServer, id: number | string) { | |||
67 | expect(video.description).to.equal('my super description') | 67 | expect(video.description).to.equal('my super description') |
68 | expect(video.tags).to.deep.equal([ 'supertag1', 'supertag2' ]) | 68 | expect(video.tags).to.deep.equal([ 'supertag1', 'supertag2' ]) |
69 | 69 | ||
70 | await testImage(server.url, 'thumbnail', video.thumbnailPath) | 70 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', video.thumbnailPath) |
71 | 71 | ||
72 | expect(video.files).to.have.lengthOf(1) | 72 | expect(video.files).to.have.lengthOf(1) |
73 | 73 | ||
@@ -119,15 +119,15 @@ describe('Test video imports', function () { | |||
119 | expect(video.name).to.equal('small video - youtube') | 119 | expect(video.name).to.equal('small video - youtube') |
120 | 120 | ||
121 | { | 121 | { |
122 | expect(video.thumbnailPath).to.match(new RegExp(`^/static/thumbnails/.+.jpg$`)) | 122 | expect(video.thumbnailPath).to.match(new RegExp(`^/lazy-static/thumbnails/.+.jpg$`)) |
123 | expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`)) | 123 | expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`)) |
124 | 124 | ||
125 | const suffix = mode === 'yt-dlp' | 125 | const suffix = mode === 'yt-dlp' |
126 | ? '_yt_dlp' | 126 | ? '_yt_dlp' |
127 | : '' | 127 | : '' |
128 | 128 | ||
129 | await testImage(servers[0].url, 'video_import_thumbnail' + suffix, video.thumbnailPath) | 129 | await testImageGeneratedByFFmpeg(servers[0].url, 'video_import_thumbnail' + suffix, video.thumbnailPath) |
130 | await testImage(servers[0].url, 'video_import_preview' + suffix, video.previewPath) | 130 | await testImageGeneratedByFFmpeg(servers[0].url, 'video_import_preview' + suffix, video.previewPath) |
131 | } | 131 | } |
132 | 132 | ||
133 | const bodyCaptions = await servers[0].captions.list({ videoId: video.id }) | 133 | const bodyCaptions = await servers[0].captions.list({ videoId: video.id }) |
@@ -266,7 +266,7 @@ describe('Test video imports', function () { | |||
266 | name: 'my super name', | 266 | name: 'my super name', |
267 | description: 'my super description', | 267 | description: 'my super description', |
268 | tags: [ 'supertag1', 'supertag2' ], | 268 | tags: [ 'supertag1', 'supertag2' ], |
269 | thumbnailfile: 'thumbnail.jpg' | 269 | thumbnailfile: 'custom-thumbnail.jpg' |
270 | } | 270 | } |
271 | }) | 271 | }) |
272 | expect(video.name).to.equal('my super name') | 272 | expect(video.name).to.equal('my super name') |
@@ -328,7 +328,7 @@ describe('Test video imports', function () { | |||
328 | '1440p': false, | 328 | '1440p': false, |
329 | '2160p': false | 329 | '2160p': false |
330 | }, | 330 | }, |
331 | webtorrent: { enabled: true }, | 331 | webVideos: { enabled: true }, |
332 | hls: { enabled: false } | 332 | hls: { enabled: false } |
333 | } | 333 | } |
334 | } | 334 | } |
diff --git a/server/tests/api/videos/video-passwords.ts b/server/tests/api/videos/video-passwords.ts new file mode 100644 index 000000000..e01a93a4d --- /dev/null +++ b/server/tests/api/videos/video-passwords.ts | |||
@@ -0,0 +1,97 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | VideoPasswordsCommand, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultAccountAvatar, | ||
11 | setDefaultChannelAvatar | ||
12 | } from '@shared/server-commands' | ||
13 | import { VideoPrivacy } from '@shared/models' | ||
14 | |||
15 | describe('Test video passwords', function () { | ||
16 | let server: PeerTubeServer | ||
17 | let videoUUID: string | ||
18 | |||
19 | let userAccessTokenServer1: string | ||
20 | |||
21 | let videoPasswords: string[] = [] | ||
22 | let command: VideoPasswordsCommand | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(30000) | ||
26 | |||
27 | server = await createSingleServer(1) | ||
28 | |||
29 | await setAccessTokensToServers([ server ]) | ||
30 | |||
31 | for (let i = 0; i < 10; i++) { | ||
32 | videoPasswords.push(`password ${i + 1}`) | ||
33 | } | ||
34 | const { uuid } = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords } }) | ||
35 | videoUUID = uuid | ||
36 | |||
37 | await setDefaultChannelAvatar(server) | ||
38 | await setDefaultAccountAvatar(server) | ||
39 | |||
40 | userAccessTokenServer1 = await server.users.generateUserAndToken('user1') | ||
41 | await setDefaultChannelAvatar(server, 'user1_channel') | ||
42 | await setDefaultAccountAvatar(server, userAccessTokenServer1) | ||
43 | |||
44 | command = server.videoPasswords | ||
45 | }) | ||
46 | |||
47 | it('Should list video passwords', async function () { | ||
48 | const body = await command.list({ videoId: videoUUID }) | ||
49 | |||
50 | expect(body.total).to.equal(10) | ||
51 | expect(body.data).to.be.an('array') | ||
52 | expect(body.data).to.have.lengthOf(10) | ||
53 | }) | ||
54 | |||
55 | it('Should filter passwords on this video', async function () { | ||
56 | const body = await command.list({ videoId: videoUUID, count: 2, start: 3, sort: 'createdAt' }) | ||
57 | |||
58 | expect(body.total).to.equal(10) | ||
59 | expect(body.data).to.be.an('array') | ||
60 | expect(body.data).to.have.lengthOf(2) | ||
61 | expect(body.data[0].password).to.equal('password 4') | ||
62 | expect(body.data[1].password).to.equal('password 5') | ||
63 | }) | ||
64 | |||
65 | it('Should update password for this video', async function () { | ||
66 | videoPasswords = [ 'my super new password 1', 'my super new password 2' ] | ||
67 | |||
68 | await command.updateAll({ videoId: videoUUID, passwords: videoPasswords }) | ||
69 | const body = await command.list({ videoId: videoUUID }) | ||
70 | expect(body.total).to.equal(2) | ||
71 | expect(body.data).to.be.an('array') | ||
72 | expect(body.data).to.have.lengthOf(2) | ||
73 | expect(body.data[0].password).to.equal('my super new password 2') | ||
74 | expect(body.data[1].password).to.equal('my super new password 1') | ||
75 | }) | ||
76 | |||
77 | it('Should delete one password', async function () { | ||
78 | { | ||
79 | const body = await command.list({ videoId: videoUUID }) | ||
80 | expect(body.total).to.equal(2) | ||
81 | expect(body.data).to.be.an('array') | ||
82 | expect(body.data).to.have.lengthOf(2) | ||
83 | await command.remove({ id: body.data[0].id, videoId: videoUUID }) | ||
84 | } | ||
85 | { | ||
86 | const body = await command.list({ videoId: videoUUID }) | ||
87 | |||
88 | expect(body.total).to.equal(1) | ||
89 | expect(body.data).to.be.an('array') | ||
90 | expect(body.data).to.have.lengthOf(1) | ||
91 | } | ||
92 | }) | ||
93 | |||
94 | after(async function () { | ||
95 | await cleanupTests([ server ]) | ||
96 | }) | ||
97 | }) | ||
diff --git a/server/tests/api/videos/video-playlist-thumbnails.ts b/server/tests/api/videos/video-playlist-thumbnails.ts index 356939b93..c274c20bf 100644 --- a/server/tests/api/videos/video-playlist-thumbnails.ts +++ b/server/tests/api/videos/video-playlist-thumbnails.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { testImage } from '@server/tests/shared' | 4 | import { testImageGeneratedByFFmpeg } from '@server/tests/shared' |
5 | import { VideoPlaylistPrivacy } from '@shared/models' | 5 | import { VideoPlaylistPrivacy } from '@shared/models' |
6 | import { | 6 | import { |
7 | cleanupTests, | 7 | cleanupTests, |
@@ -83,7 +83,7 @@ describe('Playlist thumbnail', function () { | |||
83 | 83 | ||
84 | for (const server of servers) { | 84 | for (const server of servers) { |
85 | const p = await getPlaylistWithoutThumbnail(server) | 85 | const p = await getPlaylistWithoutThumbnail(server) |
86 | await testImage(server.url, 'thumbnail-playlist', p.thumbnailPath) | 86 | await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', p.thumbnailPath) |
87 | } | 87 | } |
88 | }) | 88 | }) |
89 | 89 | ||
@@ -95,7 +95,7 @@ describe('Playlist thumbnail', function () { | |||
95 | displayName: 'playlist with thumbnail', | 95 | displayName: 'playlist with thumbnail', |
96 | privacy: VideoPlaylistPrivacy.PUBLIC, | 96 | privacy: VideoPlaylistPrivacy.PUBLIC, |
97 | videoChannelId: servers[1].store.channel.id, | 97 | videoChannelId: servers[1].store.channel.id, |
98 | thumbnailfile: 'thumbnail.jpg' | 98 | thumbnailfile: 'custom-thumbnail.jpg' |
99 | } | 99 | } |
100 | }) | 100 | }) |
101 | playlistWithThumbnailId = created.id | 101 | playlistWithThumbnailId = created.id |
@@ -110,7 +110,7 @@ describe('Playlist thumbnail', function () { | |||
110 | 110 | ||
111 | for (const server of servers) { | 111 | for (const server of servers) { |
112 | const p = await getPlaylistWithThumbnail(server) | 112 | const p = await getPlaylistWithThumbnail(server) |
113 | await testImage(server.url, 'thumbnail', p.thumbnailPath) | 113 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath) |
114 | } | 114 | } |
115 | }) | 115 | }) |
116 | 116 | ||
@@ -135,7 +135,7 @@ describe('Playlist thumbnail', function () { | |||
135 | 135 | ||
136 | for (const server of servers) { | 136 | for (const server of servers) { |
137 | const p = await getPlaylistWithoutThumbnail(server) | 137 | const p = await getPlaylistWithoutThumbnail(server) |
138 | await testImage(server.url, 'thumbnail-playlist', p.thumbnailPath) | 138 | await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', p.thumbnailPath) |
139 | } | 139 | } |
140 | }) | 140 | }) |
141 | 141 | ||
@@ -160,7 +160,7 @@ describe('Playlist thumbnail', function () { | |||
160 | 160 | ||
161 | for (const server of servers) { | 161 | for (const server of servers) { |
162 | const p = await getPlaylistWithThumbnail(server) | 162 | const p = await getPlaylistWithThumbnail(server) |
163 | await testImage(server.url, 'thumbnail', p.thumbnailPath) | 163 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath) |
164 | } | 164 | } |
165 | }) | 165 | }) |
166 | 166 | ||
@@ -176,7 +176,7 @@ describe('Playlist thumbnail', function () { | |||
176 | 176 | ||
177 | for (const server of servers) { | 177 | for (const server of servers) { |
178 | const p = await getPlaylistWithoutThumbnail(server) | 178 | const p = await getPlaylistWithoutThumbnail(server) |
179 | await testImage(server.url, 'thumbnail-playlist', p.thumbnailPath) | 179 | await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', p.thumbnailPath) |
180 | } | 180 | } |
181 | }) | 181 | }) |
182 | 182 | ||
@@ -192,7 +192,7 @@ describe('Playlist thumbnail', function () { | |||
192 | 192 | ||
193 | for (const server of servers) { | 193 | for (const server of servers) { |
194 | const p = await getPlaylistWithThumbnail(server) | 194 | const p = await getPlaylistWithThumbnail(server) |
195 | await testImage(server.url, 'thumbnail', p.thumbnailPath) | 195 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath) |
196 | } | 196 | } |
197 | }) | 197 | }) |
198 | 198 | ||
@@ -224,7 +224,7 @@ describe('Playlist thumbnail', function () { | |||
224 | 224 | ||
225 | for (const server of servers) { | 225 | for (const server of servers) { |
226 | const p = await getPlaylistWithThumbnail(server) | 226 | const p = await getPlaylistWithThumbnail(server) |
227 | await testImage(server.url, 'thumbnail', p.thumbnailPath) | 227 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath) |
228 | } | 228 | } |
229 | }) | 229 | }) |
230 | 230 | ||
diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts index d9c5bdf16..3bfa874cb 100644 --- a/server/tests/api/videos/video-playlists.ts +++ b/server/tests/api/videos/video-playlists.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { checkPlaylistFilesWereRemoved, testImage } from '@server/tests/shared' | 4 | import { checkPlaylistFilesWereRemoved, testImageGeneratedByFFmpeg } from '@server/tests/shared' |
5 | import { wait } from '@shared/core-utils' | 5 | import { wait } from '@shared/core-utils' |
6 | import { uuidToShort } from '@shared/extra-utils' | 6 | import { uuidToShort } from '@shared/extra-utils' |
7 | import { | 7 | import { |
@@ -133,7 +133,7 @@ describe('Test video playlists', function () { | |||
133 | displayName: 'my super playlist', | 133 | displayName: 'my super playlist', |
134 | privacy: VideoPlaylistPrivacy.PUBLIC, | 134 | privacy: VideoPlaylistPrivacy.PUBLIC, |
135 | description: 'my super description', | 135 | description: 'my super description', |
136 | thumbnailfile: 'thumbnail.jpg', | 136 | thumbnailfile: 'custom-thumbnail.jpg', |
137 | videoChannelId: servers[0].store.channel.id | 137 | videoChannelId: servers[0].store.channel.id |
138 | } | 138 | } |
139 | }) | 139 | }) |
@@ -225,7 +225,7 @@ describe('Test video playlists', function () { | |||
225 | displayName: 'my super playlist', | 225 | displayName: 'my super playlist', |
226 | privacy: VideoPlaylistPrivacy.PUBLIC, | 226 | privacy: VideoPlaylistPrivacy.PUBLIC, |
227 | description: 'my super description', | 227 | description: 'my super description', |
228 | thumbnailfile: 'thumbnail.jpg', | 228 | thumbnailfile: 'custom-thumbnail.jpg', |
229 | videoChannelId: servers[0].store.channel.id | 229 | videoChannelId: servers[0].store.channel.id |
230 | } | 230 | } |
231 | }) | 231 | }) |
@@ -286,7 +286,7 @@ describe('Test video playlists', function () { | |||
286 | attributes: { | 286 | attributes: { |
287 | displayName: 'playlist 3', | 287 | displayName: 'playlist 3', |
288 | privacy: VideoPlaylistPrivacy.PUBLIC, | 288 | privacy: VideoPlaylistPrivacy.PUBLIC, |
289 | thumbnailfile: 'thumbnail.jpg', | 289 | thumbnailfile: 'custom-thumbnail.jpg', |
290 | videoChannelId: servers[1].store.channel.id | 290 | videoChannelId: servers[1].store.channel.id |
291 | } | 291 | } |
292 | }) | 292 | }) |
@@ -314,11 +314,11 @@ describe('Test video playlists', function () { | |||
314 | 314 | ||
315 | const playlist2 = body.data.find(p => p.displayName === 'playlist 2') | 315 | const playlist2 = body.data.find(p => p.displayName === 'playlist 2') |
316 | expect(playlist2).to.not.be.undefined | 316 | expect(playlist2).to.not.be.undefined |
317 | await testImage(server.url, 'thumbnail-playlist', playlist2.thumbnailPath) | 317 | await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', playlist2.thumbnailPath) |
318 | 318 | ||
319 | const playlist3 = body.data.find(p => p.displayName === 'playlist 3') | 319 | const playlist3 = body.data.find(p => p.displayName === 'playlist 3') |
320 | expect(playlist3).to.not.be.undefined | 320 | expect(playlist3).to.not.be.undefined |
321 | await testImage(server.url, 'thumbnail', playlist3.thumbnailPath) | 321 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', playlist3.thumbnailPath) |
322 | } | 322 | } |
323 | 323 | ||
324 | const body = await servers[2].playlists.list({ start: 0, count: 5 }) | 324 | const body = await servers[2].playlists.list({ start: 0, count: 5 }) |
@@ -336,7 +336,7 @@ describe('Test video playlists', function () { | |||
336 | 336 | ||
337 | const playlist2 = body.data.find(p => p.displayName === 'playlist 2') | 337 | const playlist2 = body.data.find(p => p.displayName === 'playlist 2') |
338 | expect(playlist2).to.not.be.undefined | 338 | expect(playlist2).to.not.be.undefined |
339 | await testImage(servers[2].url, 'thumbnail-playlist', playlist2.thumbnailPath) | 339 | await testImageGeneratedByFFmpeg(servers[2].url, 'thumbnail-playlist', playlist2.thumbnailPath) |
340 | 340 | ||
341 | expect(body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined | 341 | expect(body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined |
342 | }) | 342 | }) |
@@ -474,7 +474,7 @@ describe('Test video playlists', function () { | |||
474 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.id, expectedStatus: 404 }) | 474 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.id, expectedStatus: 404 }) |
475 | }) | 475 | }) |
476 | 476 | ||
477 | it('Should get unlisted plyaylist using uuid or shortUUID', async function () { | 477 | it('Should get unlisted playlist using uuid or shortUUID', async function () { |
478 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.uuid }) | 478 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.uuid }) |
479 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.shortUUID }) | 479 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.shortUUID }) |
480 | }) | 480 | }) |
@@ -502,7 +502,7 @@ describe('Test video playlists', function () { | |||
502 | displayName: 'playlist 3 updated', | 502 | displayName: 'playlist 3 updated', |
503 | description: 'description updated', | 503 | description: 'description updated', |
504 | privacy: VideoPlaylistPrivacy.UNLISTED, | 504 | privacy: VideoPlaylistPrivacy.UNLISTED, |
505 | thumbnailfile: 'thumbnail.jpg', | 505 | thumbnailfile: 'custom-thumbnail.jpg', |
506 | videoChannelId: servers[1].store.channel.id | 506 | videoChannelId: servers[1].store.channel.id |
507 | }, | 507 | }, |
508 | playlistId: playlistServer2Id2 | 508 | playlistId: playlistServer2Id2 |
@@ -686,7 +686,7 @@ describe('Test video playlists', function () { | |||
686 | await waitJobs(servers) | 686 | await waitJobs(servers) |
687 | }) | 687 | }) |
688 | 688 | ||
689 | it('Should update the element type if the video is private', async function () { | 689 | it('Should update the element type if the video is private/password protected', async function () { |
690 | this.timeout(20000) | 690 | this.timeout(20000) |
691 | 691 | ||
692 | const name = 'video 89' | 692 | const name = 'video 89' |
@@ -703,6 +703,19 @@ describe('Test video playlists', function () { | |||
703 | } | 703 | } |
704 | 704 | ||
705 | { | 705 | { |
706 | await servers[0].videos.update({ | ||
707 | id: video1, | ||
708 | attributes: { privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords: [ 'password' ] } | ||
709 | }) | ||
710 | await waitJobs(servers) | ||
711 | |||
712 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
713 | await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3) | ||
714 | await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3) | ||
715 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) | ||
716 | } | ||
717 | |||
718 | { | ||
706 | await servers[0].videos.update({ id: video1, attributes: { privacy: VideoPrivacy.PUBLIC } }) | 719 | await servers[0].videos.update({ id: video1, attributes: { privacy: VideoPrivacy.PUBLIC } }) |
707 | await waitJobs(servers) | 720 | await waitJobs(servers) |
708 | 721 | ||
diff --git a/server/tests/api/videos/video-static-file-privacy.ts b/server/tests/api/videos/video-static-file-privacy.ts index 542848533..0a9864134 100644 --- a/server/tests/api/videos/video-static-file-privacy.ts +++ b/server/tests/api/videos/video-static-file-privacy.ts | |||
@@ -41,7 +41,7 @@ describe('Test video static file privacy', function () { | |||
41 | 41 | ||
42 | for (const file of video.files) { | 42 | for (const file of video.files) { |
43 | expect(file.fileDownloadUrl).to.not.include('/private/') | 43 | expect(file.fileDownloadUrl).to.not.include('/private/') |
44 | expectStartWith(file.fileUrl, server.url + '/static/webseed/private/') | 44 | expectStartWith(file.fileUrl, server.url + '/static/web-videos/private/') |
45 | 45 | ||
46 | const torrent = await parseTorrentVideo(server, file) | 46 | const torrent = await parseTorrentVideo(server, file) |
47 | expect(torrent.urlList).to.have.lengthOf(0) | 47 | expect(torrent.urlList).to.have.lengthOf(0) |
@@ -90,7 +90,7 @@ describe('Test video static file privacy', function () { | |||
90 | } | 90 | } |
91 | } | 91 | } |
92 | 92 | ||
93 | it('Should upload a private/internal video and have a private static path', async function () { | 93 | it('Should upload a private/internal/password protected video and have a private static path', async function () { |
94 | this.timeout(120000) | 94 | this.timeout(120000) |
95 | 95 | ||
96 | for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { | 96 | for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { |
@@ -99,6 +99,15 @@ describe('Test video static file privacy', function () { | |||
99 | 99 | ||
100 | await checkPrivateFiles(uuid) | 100 | await checkPrivateFiles(uuid) |
101 | } | 101 | } |
102 | |||
103 | const { uuid } = await server.videos.quickUpload({ | ||
104 | name: 'video', | ||
105 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
106 | videoPasswords: [ 'my super password' ] | ||
107 | }) | ||
108 | await waitJobs([ server ]) | ||
109 | |||
110 | await checkPrivateFiles(uuid) | ||
102 | }) | 111 | }) |
103 | 112 | ||
104 | it('Should upload a public video and update it as private/internal to have a private static path', async function () { | 113 | it('Should upload a public video and update it as private/internal to have a private static path', async function () { |
@@ -185,8 +194,9 @@ describe('Test video static file privacy', function () { | |||
185 | expectedStatus: HttpStatusCode | 194 | expectedStatus: HttpStatusCode |
186 | token: string | 195 | token: string |
187 | videoFileToken: string | 196 | videoFileToken: string |
197 | videoPassword?: string | ||
188 | }) { | 198 | }) { |
189 | const { id, expectedStatus, token, videoFileToken } = options | 199 | const { id, expectedStatus, token, videoFileToken, videoPassword } = options |
190 | 200 | ||
191 | const video = await server.videos.getWithToken({ id }) | 201 | const video = await server.videos.getWithToken({ id }) |
192 | 202 | ||
@@ -196,6 +206,12 @@ describe('Test video static file privacy', function () { | |||
196 | 206 | ||
197 | await makeRawRequest({ url: file.fileUrl, query: { videoFileToken }, expectedStatus }) | 207 | await makeRawRequest({ url: file.fileUrl, query: { videoFileToken }, expectedStatus }) |
198 | await makeRawRequest({ url: file.fileDownloadUrl, query: { videoFileToken }, expectedStatus }) | 208 | await makeRawRequest({ url: file.fileDownloadUrl, query: { videoFileToken }, expectedStatus }) |
209 | |||
210 | if (videoPassword) { | ||
211 | const headers = { 'x-peertube-video-password': videoPassword } | ||
212 | await makeRawRequest({ url: file.fileUrl, headers, expectedStatus }) | ||
213 | await makeRawRequest({ url: file.fileDownloadUrl, headers, expectedStatus }) | ||
214 | } | ||
199 | } | 215 | } |
200 | 216 | ||
201 | const hls = video.streamingPlaylists[0] | 217 | const hls = video.streamingPlaylists[0] |
@@ -204,6 +220,12 @@ describe('Test video static file privacy', function () { | |||
204 | 220 | ||
205 | await makeRawRequest({ url: hls.playlistUrl, query: { videoFileToken }, expectedStatus }) | 221 | await makeRawRequest({ url: hls.playlistUrl, query: { videoFileToken }, expectedStatus }) |
206 | await makeRawRequest({ url: hls.segmentsSha256Url, query: { videoFileToken }, expectedStatus }) | 222 | await makeRawRequest({ url: hls.segmentsSha256Url, query: { videoFileToken }, expectedStatus }) |
223 | |||
224 | if (videoPassword) { | ||
225 | const headers = { 'x-peertube-video-password': videoPassword } | ||
226 | await makeRawRequest({ url: hls.playlistUrl, token: null, headers, expectedStatus }) | ||
227 | await makeRawRequest({ url: hls.segmentsSha256Url, token: null, headers, expectedStatus }) | ||
228 | } | ||
207 | } | 229 | } |
208 | 230 | ||
209 | before(async function () { | 231 | before(async function () { |
@@ -216,13 +238,53 @@ describe('Test video static file privacy', function () { | |||
216 | it('Should not be able to access a private video files without OAuth token and file token', async function () { | 238 | it('Should not be able to access a private video files without OAuth token and file token', async function () { |
217 | this.timeout(120000) | 239 | this.timeout(120000) |
218 | 240 | ||
219 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.INTERNAL }) | 241 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) |
220 | await waitJobs([ server ]) | 242 | await waitJobs([ server ]) |
221 | 243 | ||
222 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: null, videoFileToken: null }) | 244 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: null, videoFileToken: null }) |
223 | }) | 245 | }) |
224 | 246 | ||
225 | it('Should not be able to access an internal video files without appropriate OAuth token and file token', async function () { | 247 | it('Should not be able to access password protected video files without OAuth token, file token and password', async function () { |
248 | this.timeout(120000) | ||
249 | const videoPassword = 'my super password' | ||
250 | |||
251 | const { uuid } = await server.videos.quickUpload({ | ||
252 | name: 'password protected video', | ||
253 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
254 | videoPasswords: [ videoPassword ] | ||
255 | }) | ||
256 | await waitJobs([ server ]) | ||
257 | |||
258 | await checkVideoFiles({ | ||
259 | id: uuid, | ||
260 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
261 | token: null, | ||
262 | videoFileToken: null, | ||
263 | videoPassword: null | ||
264 | }) | ||
265 | }) | ||
266 | |||
267 | it('Should not be able to access an password video files with incorrect OAuth token, file token and password', async function () { | ||
268 | this.timeout(120000) | ||
269 | const videoPassword = 'my super password' | ||
270 | |||
271 | const { uuid } = await server.videos.quickUpload({ | ||
272 | name: 'password protected video', | ||
273 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
274 | videoPasswords: [ videoPassword ] | ||
275 | }) | ||
276 | await waitJobs([ server ]) | ||
277 | |||
278 | await checkVideoFiles({ | ||
279 | id: uuid, | ||
280 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
281 | token: userToken, | ||
282 | videoFileToken: unrelatedFileToken, | ||
283 | videoPassword: 'incorrectPassword' | ||
284 | }) | ||
285 | }) | ||
286 | |||
287 | it('Should not be able to access an private video files without appropriate OAuth token and file token', async function () { | ||
226 | this.timeout(120000) | 288 | this.timeout(120000) |
227 | 289 | ||
228 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | 290 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) |
@@ -247,6 +309,23 @@ describe('Test video static file privacy', function () { | |||
247 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken }) | 309 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken }) |
248 | }) | 310 | }) |
249 | 311 | ||
312 | it('Should be able to access a password protected video files with appropriate OAuth token or file token', async function () { | ||
313 | this.timeout(120000) | ||
314 | const videoPassword = 'my super password' | ||
315 | |||
316 | const { uuid } = await server.videos.quickUpload({ | ||
317 | name: 'video', | ||
318 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
319 | videoPasswords: [ videoPassword ] | ||
320 | }) | ||
321 | |||
322 | const videoFileToken = await server.videoToken.getVideoFileToken({ token: null, videoId: uuid, videoPassword }) | ||
323 | |||
324 | await waitJobs([ server ]) | ||
325 | |||
326 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken, videoPassword }) | ||
327 | }) | ||
328 | |||
250 | it('Should reinject video file token', async function () { | 329 | it('Should reinject video file token', async function () { |
251 | this.timeout(120000) | 330 | this.timeout(120000) |
252 | 331 | ||
@@ -294,13 +373,20 @@ describe('Test video static file privacy', function () { | |||
294 | let permanentLiveId: string | 373 | let permanentLiveId: string |
295 | let permanentLive: LiveVideo | 374 | let permanentLive: LiveVideo |
296 | 375 | ||
376 | let passwordProtectedLiveId: string | ||
377 | let passwordProtectedLive: LiveVideo | ||
378 | |||
379 | const correctPassword = 'my super password' | ||
380 | |||
297 | let unrelatedFileToken: string | 381 | let unrelatedFileToken: string |
298 | 382 | ||
299 | async function checkLiveFiles (live: LiveVideo, liveId: string) { | 383 | async function checkLiveFiles (options: { live: LiveVideo, liveId: string, videoPassword?: string }) { |
384 | const { live, liveId, videoPassword } = options | ||
300 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | 385 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) |
301 | await server.live.waitUntilPublished({ videoId: liveId }) | 386 | await server.live.waitUntilPublished({ videoId: liveId }) |
302 | 387 | ||
303 | const video = await server.videos.getWithToken({ id: liveId }) | 388 | const video = await server.videos.getWithToken({ id: liveId }) |
389 | |||
304 | const fileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) | 390 | const fileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) |
305 | 391 | ||
306 | const hls = video.streamingPlaylists[0] | 392 | const hls = video.streamingPlaylists[0] |
@@ -314,6 +400,16 @@ describe('Test video static file privacy', function () { | |||
314 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 400 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
315 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 401 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
316 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 402 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
403 | |||
404 | if (videoPassword) { | ||
405 | await makeRawRequest({ url, headers: { 'x-peertube-video-password': videoPassword }, expectedStatus: HttpStatusCode.OK_200 }) | ||
406 | await makeRawRequest({ | ||
407 | url, | ||
408 | headers: { 'x-peertube-video-password': 'incorrectPassword' }, | ||
409 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
410 | }) | ||
411 | } | ||
412 | |||
317 | } | 413 | } |
318 | 414 | ||
319 | await stopFfmpeg(ffmpegCommand) | 415 | await stopFfmpeg(ffmpegCommand) |
@@ -381,18 +477,35 @@ describe('Test video static file privacy', function () { | |||
381 | permanentLiveId = video.uuid | 477 | permanentLiveId = video.uuid |
382 | permanentLive = live | 478 | permanentLive = live |
383 | } | 479 | } |
480 | |||
481 | { | ||
482 | const { video, live } = await server.live.quickCreate({ | ||
483 | saveReplay: false, | ||
484 | permanentLive: false, | ||
485 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
486 | videoPasswords: [ correctPassword ] | ||
487 | }) | ||
488 | passwordProtectedLiveId = video.uuid | ||
489 | passwordProtectedLive = live | ||
490 | } | ||
384 | }) | 491 | }) |
385 | 492 | ||
386 | it('Should create a private normal live and have a private static path', async function () { | 493 | it('Should create a private normal live and have a private static path', async function () { |
387 | this.timeout(240000) | 494 | this.timeout(240000) |
388 | 495 | ||
389 | await checkLiveFiles(normalLive, normalLiveId) | 496 | await checkLiveFiles({ live: normalLive, liveId: normalLiveId }) |
390 | }) | 497 | }) |
391 | 498 | ||
392 | it('Should create a private permanent live and have a private static path', async function () { | 499 | it('Should create a private permanent live and have a private static path', async function () { |
393 | this.timeout(240000) | 500 | this.timeout(240000) |
394 | 501 | ||
395 | await checkLiveFiles(permanentLive, permanentLiveId) | 502 | await checkLiveFiles({ live: permanentLive, liveId: permanentLiveId }) |
503 | }) | ||
504 | |||
505 | it('Should create a password protected live and have a private static path', async function () { | ||
506 | this.timeout(240000) | ||
507 | |||
508 | await checkLiveFiles({ live: passwordProtectedLive, liveId: passwordProtectedLiveId, videoPassword: correctPassword }) | ||
396 | }) | 509 | }) |
397 | 510 | ||
398 | it('Should reinject video file token on permanent live', async function () { | 511 | it('Should reinject video file token on permanent live', async function () { |
diff --git a/server/tests/api/videos/video-storyboard.ts b/server/tests/api/videos/video-storyboard.ts new file mode 100644 index 000000000..fc4b4450f --- /dev/null +++ b/server/tests/api/videos/video-storyboard.ts | |||
@@ -0,0 +1,213 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { readdir } from 'fs-extra' | ||
5 | import { basename } from 'path' | ||
6 | import { FIXTURE_URLS } from '@server/tests/shared' | ||
7 | import { areHttpImportTestsDisabled } from '@shared/core-utils' | ||
8 | import { HttpStatusCode, VideoPrivacy } from '@shared/models' | ||
9 | import { | ||
10 | cleanupTests, | ||
11 | createMultipleServers, | ||
12 | doubleFollow, | ||
13 | makeGetRequest, | ||
14 | PeerTubeServer, | ||
15 | sendRTMPStream, | ||
16 | setAccessTokensToServers, | ||
17 | setDefaultVideoChannel, | ||
18 | stopFfmpeg, | ||
19 | waitJobs | ||
20 | } from '@shared/server-commands' | ||
21 | |||
22 | async function checkStoryboard (options: { | ||
23 | server: PeerTubeServer | ||
24 | uuid: string | ||
25 | tilesCount?: number | ||
26 | minSize?: number | ||
27 | }) { | ||
28 | const { server, uuid, tilesCount, minSize = 1000 } = options | ||
29 | |||
30 | const { storyboards } = await server.storyboard.list({ id: uuid }) | ||
31 | |||
32 | expect(storyboards).to.have.lengthOf(1) | ||
33 | |||
34 | const storyboard = storyboards[0] | ||
35 | |||
36 | expect(storyboard.spriteDuration).to.equal(1) | ||
37 | expect(storyboard.spriteHeight).to.equal(108) | ||
38 | expect(storyboard.spriteWidth).to.equal(192) | ||
39 | expect(storyboard.storyboardPath).to.exist | ||
40 | |||
41 | if (tilesCount) { | ||
42 | expect(storyboard.totalWidth).to.equal(192 * Math.min(tilesCount, 10)) | ||
43 | expect(storyboard.totalHeight).to.equal(108 * Math.max((tilesCount / 10), 1)) | ||
44 | } | ||
45 | |||
46 | const { body } = await makeGetRequest({ url: server.url, path: storyboard.storyboardPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
47 | expect(body.length).to.be.above(minSize) | ||
48 | } | ||
49 | |||
50 | describe('Test video storyboard', function () { | ||
51 | let servers: PeerTubeServer[] | ||
52 | |||
53 | let baseUUID: string | ||
54 | |||
55 | before(async function () { | ||
56 | this.timeout(120000) | ||
57 | |||
58 | servers = await createMultipleServers(2) | ||
59 | await setAccessTokensToServers(servers) | ||
60 | await setDefaultVideoChannel(servers) | ||
61 | |||
62 | await doubleFollow(servers[0], servers[1]) | ||
63 | }) | ||
64 | |||
65 | it('Should generate a storyboard after upload without transcoding', async function () { | ||
66 | this.timeout(60000) | ||
67 | |||
68 | // 5s video | ||
69 | const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_short.webm' }) | ||
70 | baseUUID = uuid | ||
71 | await waitJobs(servers) | ||
72 | |||
73 | for (const server of servers) { | ||
74 | await checkStoryboard({ server, uuid, tilesCount: 5 }) | ||
75 | } | ||
76 | }) | ||
77 | |||
78 | it('Should generate a storyboard after upload without transcoding with a long video', async function () { | ||
79 | this.timeout(60000) | ||
80 | |||
81 | // 124s video | ||
82 | const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_very_long_10p.mp4' }) | ||
83 | await waitJobs(servers) | ||
84 | |||
85 | for (const server of servers) { | ||
86 | await checkStoryboard({ server, uuid, tilesCount: 100 }) | ||
87 | } | ||
88 | }) | ||
89 | |||
90 | it('Should generate a storyboard after upload with transcoding', async function () { | ||
91 | this.timeout(60000) | ||
92 | |||
93 | await servers[0].config.enableMinimumTranscoding() | ||
94 | |||
95 | // 5s video | ||
96 | const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_short.webm' }) | ||
97 | await waitJobs(servers) | ||
98 | |||
99 | for (const server of servers) { | ||
100 | await checkStoryboard({ server, uuid, tilesCount: 5 }) | ||
101 | } | ||
102 | }) | ||
103 | |||
104 | it('Should generate a storyboard after an audio upload', async function () { | ||
105 | this.timeout(60000) | ||
106 | |||
107 | // 6s audio | ||
108 | const attributes = { name: 'audio', fixture: 'sample.ogg' } | ||
109 | const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' }) | ||
110 | await waitJobs(servers) | ||
111 | |||
112 | for (const server of servers) { | ||
113 | try { | ||
114 | await checkStoryboard({ server, uuid, tilesCount: 6, minSize: 250 }) | ||
115 | } catch { // FIXME: to remove after ffmpeg CI upgrade, ffmpeg CI version (4.3) generates a 7.6s length video | ||
116 | await checkStoryboard({ server, uuid, tilesCount: 8, minSize: 250 }) | ||
117 | } | ||
118 | } | ||
119 | }) | ||
120 | |||
121 | it('Should generate a storyboard after HTTP import', async function () { | ||
122 | this.timeout(60000) | ||
123 | |||
124 | if (areHttpImportTestsDisabled()) return | ||
125 | |||
126 | // 3s video | ||
127 | const { video } = await servers[0].imports.importVideo({ | ||
128 | attributes: { | ||
129 | targetUrl: FIXTURE_URLS.goodVideo, | ||
130 | channelId: servers[0].store.channel.id, | ||
131 | privacy: VideoPrivacy.PUBLIC | ||
132 | } | ||
133 | }) | ||
134 | await waitJobs(servers) | ||
135 | |||
136 | for (const server of servers) { | ||
137 | await checkStoryboard({ server, uuid: video.uuid, tilesCount: 3 }) | ||
138 | } | ||
139 | }) | ||
140 | |||
141 | it('Should generate a storyboard after torrent import', async function () { | ||
142 | this.timeout(60000) | ||
143 | |||
144 | if (areHttpImportTestsDisabled()) return | ||
145 | |||
146 | // 10s video | ||
147 | const { video } = await servers[0].imports.importVideo({ | ||
148 | attributes: { | ||
149 | magnetUri: FIXTURE_URLS.magnet, | ||
150 | channelId: servers[0].store.channel.id, | ||
151 | privacy: VideoPrivacy.PUBLIC | ||
152 | } | ||
153 | }) | ||
154 | await waitJobs(servers) | ||
155 | |||
156 | for (const server of servers) { | ||
157 | await checkStoryboard({ server, uuid: video.uuid, tilesCount: 10 }) | ||
158 | } | ||
159 | }) | ||
160 | |||
161 | it('Should generate a storyboard after a live', async function () { | ||
162 | this.timeout(240000) | ||
163 | |||
164 | await servers[0].config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' }) | ||
165 | |||
166 | const { live, video } = await servers[0].live.quickCreate({ | ||
167 | saveReplay: true, | ||
168 | permanentLive: false, | ||
169 | privacy: VideoPrivacy.PUBLIC | ||
170 | }) | ||
171 | |||
172 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
173 | await servers[0].live.waitUntilPublished({ videoId: video.id }) | ||
174 | |||
175 | await stopFfmpeg(ffmpegCommand) | ||
176 | |||
177 | await servers[0].live.waitUntilReplacedByReplay({ videoId: video.id }) | ||
178 | await waitJobs(servers) | ||
179 | |||
180 | for (const server of servers) { | ||
181 | await checkStoryboard({ server, uuid: video.uuid }) | ||
182 | } | ||
183 | }) | ||
184 | |||
185 | it('Should cleanup storyboards on video deletion', async function () { | ||
186 | this.timeout(60000) | ||
187 | |||
188 | const { storyboards } = await servers[0].storyboard.list({ id: baseUUID }) | ||
189 | const storyboardName = basename(storyboards[0].storyboardPath) | ||
190 | |||
191 | const listFiles = () => { | ||
192 | const storyboardPath = servers[0].getDirectoryPath('storyboards') | ||
193 | return readdir(storyboardPath) | ||
194 | } | ||
195 | |||
196 | { | ||
197 | const storyboads = await listFiles() | ||
198 | expect(storyboads).to.include(storyboardName) | ||
199 | } | ||
200 | |||
201 | await servers[0].videos.remove({ id: baseUUID }) | ||
202 | await waitJobs(servers) | ||
203 | |||
204 | { | ||
205 | const storyboads = await listFiles() | ||
206 | expect(storyboads).to.not.include(storyboardName) | ||
207 | } | ||
208 | }) | ||
209 | |||
210 | after(async function () { | ||
211 | await cleanupTests(servers) | ||
212 | }) | ||
213 | }) | ||
diff --git a/server/tests/api/videos/videos-common-filters.ts b/server/tests/api/videos/videos-common-filters.ts index 30251706b..73c066bfb 100644 --- a/server/tests/api/videos/videos-common-filters.ts +++ b/server/tests/api/videos/videos-common-filters.ts | |||
@@ -154,7 +154,7 @@ describe('Test videos filter', function () { | |||
154 | server: PeerTubeServer | 154 | server: PeerTubeServer |
155 | path: string | 155 | path: string |
156 | isLocal?: boolean | 156 | isLocal?: boolean |
157 | hasWebtorrentFiles?: boolean | 157 | hasWebVideoFiles?: boolean |
158 | hasHLSFiles?: boolean | 158 | hasHLSFiles?: boolean |
159 | include?: VideoInclude | 159 | include?: VideoInclude |
160 | privacyOneOf?: VideoPrivacy[] | 160 | privacyOneOf?: VideoPrivacy[] |
@@ -174,7 +174,7 @@ describe('Test videos filter', function () { | |||
174 | 'include', | 174 | 'include', |
175 | 'category', | 175 | 'category', |
176 | 'tagsAllOf', | 176 | 'tagsAllOf', |
177 | 'hasWebtorrentFiles', | 177 | 'hasWebVideoFiles', |
178 | 'hasHLSFiles', | 178 | 'hasHLSFiles', |
179 | 'privacyOneOf', | 179 | 'privacyOneOf', |
180 | 'excludeAlreadyWatched' | 180 | 'excludeAlreadyWatched' |
@@ -463,14 +463,14 @@ describe('Test videos filter', function () { | |||
463 | } | 463 | } |
464 | }) | 464 | }) |
465 | 465 | ||
466 | it('Should filter by HLS or WebTorrent files', async function () { | 466 | it('Should filter by HLS or Web Video files', async function () { |
467 | this.timeout(360000) | 467 | this.timeout(360000) |
468 | 468 | ||
469 | const finderFactory = (name: string) => (videos: Video[]) => videos.some(v => v.name === name) | 469 | const finderFactory = (name: string) => (videos: Video[]) => videos.some(v => v.name === name) |
470 | 470 | ||
471 | await servers[0].config.enableTranscoding(true, false) | 471 | await servers[0].config.enableTranscoding(true, false) |
472 | await servers[0].videos.upload({ attributes: { name: 'webtorrent video' } }) | 472 | await servers[0].videos.upload({ attributes: { name: 'web video video' } }) |
473 | const hasWebtorrent = finderFactory('webtorrent video') | 473 | const hasWebVideo = finderFactory('web video video') |
474 | 474 | ||
475 | await waitJobs(servers) | 475 | await waitJobs(servers) |
476 | 476 | ||
@@ -481,24 +481,24 @@ describe('Test videos filter', function () { | |||
481 | await waitJobs(servers) | 481 | await waitJobs(servers) |
482 | 482 | ||
483 | await servers[0].config.enableTranscoding(true, true) | 483 | await servers[0].config.enableTranscoding(true, true) |
484 | await servers[0].videos.upload({ attributes: { name: 'hls and webtorrent video' } }) | 484 | await servers[0].videos.upload({ attributes: { name: 'hls and web video video' } }) |
485 | const hasBoth = finderFactory('hls and webtorrent video') | 485 | const hasBoth = finderFactory('hls and web video video') |
486 | 486 | ||
487 | await waitJobs(servers) | 487 | await waitJobs(servers) |
488 | 488 | ||
489 | for (const path of paths) { | 489 | for (const path of paths) { |
490 | { | 490 | { |
491 | const videos = await listVideos({ server: servers[0], path, hasWebtorrentFiles: true }) | 491 | const videos = await listVideos({ server: servers[0], path, hasWebVideoFiles: true }) |
492 | 492 | ||
493 | expect(hasWebtorrent(videos)).to.be.true | 493 | expect(hasWebVideo(videos)).to.be.true |
494 | expect(hasHLS(videos)).to.be.false | 494 | expect(hasHLS(videos)).to.be.false |
495 | expect(hasBoth(videos)).to.be.true | 495 | expect(hasBoth(videos)).to.be.true |
496 | } | 496 | } |
497 | 497 | ||
498 | { | 498 | { |
499 | const videos = await listVideos({ server: servers[0], path, hasWebtorrentFiles: false }) | 499 | const videos = await listVideos({ server: servers[0], path, hasWebVideoFiles: false }) |
500 | 500 | ||
501 | expect(hasWebtorrent(videos)).to.be.false | 501 | expect(hasWebVideo(videos)).to.be.false |
502 | expect(hasHLS(videos)).to.be.true | 502 | expect(hasHLS(videos)).to.be.true |
503 | expect(hasBoth(videos)).to.be.false | 503 | expect(hasBoth(videos)).to.be.false |
504 | } | 504 | } |
@@ -506,7 +506,7 @@ describe('Test videos filter', function () { | |||
506 | { | 506 | { |
507 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true }) | 507 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true }) |
508 | 508 | ||
509 | expect(hasWebtorrent(videos)).to.be.false | 509 | expect(hasWebVideo(videos)).to.be.false |
510 | expect(hasHLS(videos)).to.be.true | 510 | expect(hasHLS(videos)).to.be.true |
511 | expect(hasBoth(videos)).to.be.true | 511 | expect(hasBoth(videos)).to.be.true |
512 | } | 512 | } |
@@ -514,23 +514,23 @@ describe('Test videos filter', function () { | |||
514 | { | 514 | { |
515 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false }) | 515 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false }) |
516 | 516 | ||
517 | expect(hasWebtorrent(videos)).to.be.true | 517 | expect(hasWebVideo(videos)).to.be.true |
518 | expect(hasHLS(videos)).to.be.false | 518 | expect(hasHLS(videos)).to.be.false |
519 | expect(hasBoth(videos)).to.be.false | 519 | expect(hasBoth(videos)).to.be.false |
520 | } | 520 | } |
521 | 521 | ||
522 | { | 522 | { |
523 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false, hasWebtorrentFiles: false }) | 523 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false, hasWebVideoFiles: false }) |
524 | 524 | ||
525 | expect(hasWebtorrent(videos)).to.be.false | 525 | expect(hasWebVideo(videos)).to.be.false |
526 | expect(hasHLS(videos)).to.be.false | 526 | expect(hasHLS(videos)).to.be.false |
527 | expect(hasBoth(videos)).to.be.false | 527 | expect(hasBoth(videos)).to.be.false |
528 | } | 528 | } |
529 | 529 | ||
530 | { | 530 | { |
531 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true, hasWebtorrentFiles: true }) | 531 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true, hasWebVideoFiles: true }) |
532 | 532 | ||
533 | expect(hasWebtorrent(videos)).to.be.false | 533 | expect(hasWebVideo(videos)).to.be.false |
534 | expect(hasHLS(videos)).to.be.false | 534 | expect(hasHLS(videos)).to.be.false |
535 | expect(hasBoth(videos)).to.be.true | 535 | expect(hasBoth(videos)).to.be.true |
536 | } | 536 | } |
diff --git a/server/tests/cli/create-generate-storyboard-job.ts b/server/tests/cli/create-generate-storyboard-job.ts new file mode 100644 index 000000000..02a4be8ae --- /dev/null +++ b/server/tests/cli/create-generate-storyboard-job.ts | |||
@@ -0,0 +1,120 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { readdir, remove } from 'fs-extra' | ||
5 | import { join } from 'path' | ||
6 | import { HttpStatusCode } from '@shared/models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createMultipleServers, | ||
10 | doubleFollow, | ||
11 | makeGetRequest, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers, | ||
14 | waitJobs | ||
15 | } from '@shared/server-commands' | ||
16 | import { SQLCommand } from '../shared' | ||
17 | |||
18 | function listStoryboardFiles (server: PeerTubeServer) { | ||
19 | const storage = server.getDirectoryPath('storyboards') | ||
20 | |||
21 | return readdir(storage) | ||
22 | } | ||
23 | |||
24 | describe('Test create generate storyboard job', function () { | ||
25 | let servers: PeerTubeServer[] = [] | ||
26 | const uuids: string[] = [] | ||
27 | let sql: SQLCommand | ||
28 | let existingStoryboardName: string | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(120000) | ||
32 | |||
33 | // Run server 2 to have transcoding enabled | ||
34 | servers = await createMultipleServers(2) | ||
35 | await setAccessTokensToServers(servers) | ||
36 | |||
37 | await doubleFollow(servers[0], servers[1]) | ||
38 | |||
39 | for (let i = 0; i < 3; i++) { | ||
40 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video ' + i }) | ||
41 | uuids.push(uuid) | ||
42 | } | ||
43 | |||
44 | await waitJobs(servers) | ||
45 | |||
46 | const storage = servers[0].getDirectoryPath('storyboards') | ||
47 | for (const storyboard of await listStoryboardFiles(servers[0])) { | ||
48 | await remove(join(storage, storyboard)) | ||
49 | } | ||
50 | |||
51 | sql = new SQLCommand(servers[0]) | ||
52 | await sql.deleteAll('storyboard') | ||
53 | |||
54 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video 4' }) | ||
55 | uuids.push(uuid) | ||
56 | |||
57 | await waitJobs(servers) | ||
58 | |||
59 | const storyboards = await listStoryboardFiles(servers[0]) | ||
60 | existingStoryboardName = storyboards[0] | ||
61 | }) | ||
62 | |||
63 | it('Should create a storyboard of a video', async function () { | ||
64 | this.timeout(120000) | ||
65 | |||
66 | for (const uuid of [ uuids[0], uuids[3] ]) { | ||
67 | const command = `npm run create-generate-storyboard-job -- -v ${uuid}` | ||
68 | await servers[0].cli.execWithEnv(command) | ||
69 | } | ||
70 | |||
71 | await waitJobs(servers) | ||
72 | |||
73 | { | ||
74 | const storyboards = await listStoryboardFiles(servers[0]) | ||
75 | expect(storyboards).to.have.lengthOf(2) | ||
76 | expect(storyboards).to.not.include(existingStoryboardName) | ||
77 | |||
78 | existingStoryboardName = storyboards[0] | ||
79 | } | ||
80 | |||
81 | for (const server of servers) { | ||
82 | for (const uuid of [ uuids[0], uuids[3] ]) { | ||
83 | const { storyboards } = await server.storyboard.list({ id: uuid }) | ||
84 | expect(storyboards).to.have.lengthOf(1) | ||
85 | |||
86 | await makeGetRequest({ url: server.url, path: storyboards[0].storyboardPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
87 | } | ||
88 | } | ||
89 | }) | ||
90 | |||
91 | it('Should create missing storyboards', async function () { | ||
92 | this.timeout(120000) | ||
93 | |||
94 | const command = `npm run create-generate-storyboard-job -- -a` | ||
95 | await servers[0].cli.execWithEnv(command) | ||
96 | |||
97 | await waitJobs(servers) | ||
98 | |||
99 | { | ||
100 | const storyboards = await listStoryboardFiles(servers[0]) | ||
101 | expect(storyboards).to.have.lengthOf(4) | ||
102 | expect(storyboards).to.include(existingStoryboardName) | ||
103 | } | ||
104 | |||
105 | for (const server of servers) { | ||
106 | for (const uuid of uuids) { | ||
107 | const { storyboards } = await server.storyboard.list({ id: uuid }) | ||
108 | expect(storyboards).to.have.lengthOf(1) | ||
109 | |||
110 | await makeGetRequest({ url: server.url, path: storyboards[0].storyboardPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
111 | } | ||
112 | } | ||
113 | }) | ||
114 | |||
115 | after(async function () { | ||
116 | await sql.cleanup() | ||
117 | |||
118 | await cleanupTests(servers) | ||
119 | }) | ||
120 | }) | ||
diff --git a/server/tests/cli/create-move-video-storage-job.ts b/server/tests/cli/create-move-video-storage-job.ts index 253fc983e..fc6a8e648 100644 --- a/server/tests/cli/create-move-video-storage-job.ts +++ b/server/tests/cli/create-move-video-storage-job.ts | |||
@@ -109,8 +109,8 @@ describe('Test create move video storage job', function () { | |||
109 | }) | 109 | }) |
110 | 110 | ||
111 | it('Should not have files on disk anymore', async function () { | 111 | it('Should not have files on disk anymore', async function () { |
112 | await checkDirectoryIsEmpty(servers[0], 'videos', [ 'private' ]) | 112 | await checkDirectoryIsEmpty(servers[0], 'web-videos', [ 'private' ]) |
113 | await checkDirectoryIsEmpty(servers[0], join('videos', 'private')) | 113 | await checkDirectoryIsEmpty(servers[0], join('web-videos', 'private')) |
114 | 114 | ||
115 | await checkDirectoryIsEmpty(servers[0], join('streaming-playlists', 'hls'), [ 'private' ]) | 115 | await checkDirectoryIsEmpty(servers[0], join('streaming-playlists', 'hls'), [ 'private' ]) |
116 | await checkDirectoryIsEmpty(servers[0], join('streaming-playlists', 'hls', 'private')) | 116 | await checkDirectoryIsEmpty(servers[0], join('streaming-playlists', 'hls', 'private')) |
diff --git a/server/tests/cli/index.ts b/server/tests/cli/index.ts index 8579be39c..94444ace3 100644 --- a/server/tests/cli/index.ts +++ b/server/tests/cli/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | // Order of the tests we want to execute | 1 | // Order of the tests we want to execute |
2 | import './create-import-video-file-job' | 2 | import './create-import-video-file-job' |
3 | import './create-generate-storyboard-job' | ||
3 | import './create-move-video-storage-job' | 4 | import './create-move-video-storage-job' |
4 | import './peertube' | 5 | import './peertube' |
5 | import './plugins' | 6 | import './plugins' |
diff --git a/server/tests/cli/prune-storage.ts b/server/tests/cli/prune-storage.ts index 8bdf2136d..561ed6a68 100644 --- a/server/tests/cli/prune-storage.ts +++ b/server/tests/cli/prune-storage.ts | |||
@@ -35,10 +35,10 @@ async function assertNotExists (server: PeerTubeServer, directory: string, subst | |||
35 | 35 | ||
36 | async function assertCountAreOkay (servers: PeerTubeServer[]) { | 36 | async function assertCountAreOkay (servers: PeerTubeServer[]) { |
37 | for (const server of servers) { | 37 | for (const server of servers) { |
38 | const videosCount = await countFiles(server, 'videos') | 38 | const videosCount = await countFiles(server, 'web-videos') |
39 | expect(videosCount).to.equal(9) // 2 videos with 4 resolutions + private directory | 39 | expect(videosCount).to.equal(9) // 2 videos with 4 resolutions + private directory |
40 | 40 | ||
41 | const privateVideosCount = await countFiles(server, 'videos/private') | 41 | const privateVideosCount = await countFiles(server, 'web-videos/private') |
42 | expect(privateVideosCount).to.equal(4) | 42 | expect(privateVideosCount).to.equal(4) |
43 | 43 | ||
44 | const torrentsCount = await countFiles(server, 'torrents') | 44 | const torrentsCount = await countFiles(server, 'torrents') |
@@ -48,7 +48,7 @@ async function assertCountAreOkay (servers: PeerTubeServer[]) { | |||
48 | expect(previewsCount).to.equal(3) | 48 | expect(previewsCount).to.equal(3) |
49 | 49 | ||
50 | const thumbnailsCount = await countFiles(server, 'thumbnails') | 50 | const thumbnailsCount = await countFiles(server, 'thumbnails') |
51 | expect(thumbnailsCount).to.equal(7) // 3 local videos, 1 local playlist, 2 remotes videos and 1 remote playlist | 51 | expect(thumbnailsCount).to.equal(5) // 3 local videos, 1 local playlist, 2 remotes videos (lazy downloaded) and 1 remote playlist |
52 | 52 | ||
53 | const avatarsCount = await countFiles(server, 'avatars') | 53 | const avatarsCount = await countFiles(server, 'avatars') |
54 | expect(avatarsCount).to.equal(4) | 54 | expect(avatarsCount).to.equal(4) |
@@ -85,7 +85,7 @@ describe('Test prune storage scripts', function () { | |||
85 | displayName: 'playlist', | 85 | displayName: 'playlist', |
86 | privacy: VideoPlaylistPrivacy.PUBLIC, | 86 | privacy: VideoPlaylistPrivacy.PUBLIC, |
87 | videoChannelId: server.store.channel.id, | 87 | videoChannelId: server.store.channel.id, |
88 | thumbnailfile: 'thumbnail.jpg' | 88 | thumbnailfile: 'custom-thumbnail.jpg' |
89 | } | 89 | } |
90 | }) | 90 | }) |
91 | } | 91 | } |
@@ -131,8 +131,8 @@ describe('Test prune storage scripts', function () { | |||
131 | it('Should create some dirty files', async function () { | 131 | it('Should create some dirty files', async function () { |
132 | for (let i = 0; i < 2; i++) { | 132 | for (let i = 0; i < 2; i++) { |
133 | { | 133 | { |
134 | const basePublic = servers[0].servers.buildDirectory('videos') | 134 | const basePublic = servers[0].servers.buildDirectory('web-videos') |
135 | const basePrivate = servers[0].servers.buildDirectory(join('videos', 'private')) | 135 | const basePrivate = servers[0].servers.buildDirectory(join('web-videos', 'private')) |
136 | 136 | ||
137 | const n1 = buildUUID() + '.mp4' | 137 | const n1 = buildUUID() + '.mp4' |
138 | const n2 = buildUUID() + '.webm' | 138 | const n2 = buildUUID() + '.webm' |
diff --git a/server/tests/cli/regenerate-thumbnails.ts b/server/tests/cli/regenerate-thumbnails.ts index 16a8adcda..66de7f79c 100644 --- a/server/tests/cli/regenerate-thumbnails.ts +++ b/server/tests/cli/regenerate-thumbnails.ts | |||
@@ -60,6 +60,9 @@ describe('Test regenerate thumbnails script', function () { | |||
60 | 60 | ||
61 | remoteVideo = await servers[0].videos.get({ id: videoUUID }) | 61 | remoteVideo = await servers[0].videos.get({ id: videoUUID }) |
62 | 62 | ||
63 | // Load remote thumbnail on disk | ||
64 | await makeGetRequest({ url: servers[0].url, path: remoteVideo.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
65 | |||
63 | thumbnailRemotePath = join(servers[0].servers.buildDirectory('thumbnails'), basename(remoteVideo.thumbnailPath)) | 66 | thumbnailRemotePath = join(servers[0].servers.buildDirectory('thumbnails'), basename(remoteVideo.thumbnailPath)) |
64 | } | 67 | } |
65 | 68 | ||
diff --git a/server/tests/client.ts b/server/tests/client.ts index e84251561..68f3a1d14 100644 --- a/server/tests/client.ts +++ b/server/tests/client.ts | |||
@@ -56,6 +56,7 @@ describe('Test a client controllers', function () { | |||
56 | let privateVideoId: string | 56 | let privateVideoId: string |
57 | let internalVideoId: string | 57 | let internalVideoId: string |
58 | let unlistedVideoId: string | 58 | let unlistedVideoId: string |
59 | let passwordProtectedVideoId: string | ||
59 | 60 | ||
60 | let playlistIds: (string | number)[] = [] | 61 | let playlistIds: (string | number)[] = [] |
61 | 62 | ||
@@ -92,7 +93,12 @@ describe('Test a client controllers', function () { | |||
92 | { | 93 | { |
93 | ({ uuid: privateVideoId } = await servers[0].videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE })); | 94 | ({ uuid: privateVideoId } = await servers[0].videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE })); |
94 | ({ uuid: unlistedVideoId } = await servers[0].videos.quickUpload({ name: 'unlisted', privacy: VideoPrivacy.UNLISTED })); | 95 | ({ uuid: unlistedVideoId } = await servers[0].videos.quickUpload({ name: 'unlisted', privacy: VideoPrivacy.UNLISTED })); |
95 | ({ uuid: internalVideoId } = await servers[0].videos.quickUpload({ name: 'internal', privacy: VideoPrivacy.INTERNAL })) | 96 | ({ uuid: internalVideoId } = await servers[0].videos.quickUpload({ name: 'internal', privacy: VideoPrivacy.INTERNAL })); |
97 | ({ uuid: passwordProtectedVideoId } = await servers[0].videos.quickUpload({ | ||
98 | name: 'password protected', | ||
99 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
100 | videoPasswords: [ 'password' ] | ||
101 | })) | ||
96 | } | 102 | } |
97 | 103 | ||
98 | // Playlist | 104 | // Playlist |
@@ -502,9 +508,9 @@ describe('Test a client controllers', function () { | |||
502 | } | 508 | } |
503 | }) | 509 | }) |
504 | 510 | ||
505 | it('Should not display internal/private video', async function () { | 511 | it('Should not display internal/private/password protected video', async function () { |
506 | for (const basePath of watchVideoBasePaths) { | 512 | for (const basePath of watchVideoBasePaths) { |
507 | for (const id of [ privateVideoId, internalVideoId ]) { | 513 | for (const id of [ privateVideoId, internalVideoId, passwordProtectedVideoId ]) { |
508 | const res = await makeGetRequest({ | 514 | const res = await makeGetRequest({ |
509 | url: servers[0].url, | 515 | url: servers[0].url, |
510 | path: basePath + id, | 516 | path: basePath + id, |
@@ -514,6 +520,7 @@ describe('Test a client controllers', function () { | |||
514 | 520 | ||
515 | expect(res.text).to.not.contain('internal') | 521 | expect(res.text).to.not.contain('internal') |
516 | expect(res.text).to.not.contain('private') | 522 | expect(res.text).to.not.contain('private') |
523 | expect(res.text).to.not.contain('password protected') | ||
517 | } | 524 | } |
518 | } | 525 | } |
519 | }) | 526 | }) |
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts index 8433c873e..1754ac466 100644 --- a/server/tests/feeds/feeds.ts +++ b/server/tests/feeds/feeds.ts | |||
@@ -47,7 +47,7 @@ describe('Test syndication feeds', () => { | |||
47 | serverHLSOnly = await createSingleServer(3, { | 47 | serverHLSOnly = await createSingleServer(3, { |
48 | transcoding: { | 48 | transcoding: { |
49 | enabled: true, | 49 | enabled: true, |
50 | webtorrent: { enabled: false }, | 50 | web_videos: { enabled: false }, |
51 | hls: { enabled: true } | 51 | hls: { enabled: true } |
52 | } | 52 | } |
53 | }) | 53 | }) |
@@ -99,6 +99,13 @@ describe('Test syndication feeds', () => { | |||
99 | await servers[0].comments.createThread({ videoId: id, text: 'comment on unlisted video' }) | 99 | await servers[0].comments.createThread({ videoId: id, text: 'comment on unlisted video' }) |
100 | } | 100 | } |
101 | 101 | ||
102 | { | ||
103 | const attributes = { name: 'password protected video', privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords: [ 'password' ] } | ||
104 | const { id } = await servers[0].videos.upload({ attributes }) | ||
105 | |||
106 | await servers[0].comments.createThread({ videoId: id, text: 'comment on password protected video' }) | ||
107 | } | ||
108 | |||
102 | await serverHLSOnly.videos.upload({ attributes: { name: 'hls only video' } }) | 109 | await serverHLSOnly.videos.upload({ attributes: { name: 'hls only video' } }) |
103 | 110 | ||
104 | await waitJobs([ ...servers, serverHLSOnly ]) | 111 | await waitJobs([ ...servers, serverHLSOnly ]) |
@@ -445,7 +452,7 @@ describe('Test syndication feeds', () => { | |||
445 | 452 | ||
446 | describe('Video comments feed', function () { | 453 | describe('Video comments feed', function () { |
447 | 454 | ||
448 | it('Should contain valid comments (covers JSON feed 1.0 endpoint) and not from unlisted videos', async function () { | 455 | it('Should contain valid comments (covers JSON feed 1.0 endpoint) and not from unlisted/password protected videos', async function () { |
449 | for (const server of servers) { | 456 | for (const server of servers) { |
450 | const json = await server.feed.getJSON({ feed: 'video-comments', ignoreCache: true }) | 457 | const json = await server.feed.getJSON({ feed: 'video-comments', ignoreCache: true }) |
451 | 458 | ||
diff --git a/server/tests/fixtures/custom-preview-big.png b/server/tests/fixtures/custom-preview-big.png new file mode 100644 index 000000000..03d171af3 --- /dev/null +++ b/server/tests/fixtures/custom-preview-big.png | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/custom-preview.jpg b/server/tests/fixtures/custom-preview.jpg new file mode 100644 index 000000000..5a039d830 --- /dev/null +++ b/server/tests/fixtures/custom-preview.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/custom-thumbnail-big.jpg b/server/tests/fixtures/custom-thumbnail-big.jpg new file mode 100644 index 000000000..08375e425 --- /dev/null +++ b/server/tests/fixtures/custom-thumbnail-big.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/custom-thumbnail.jpg b/server/tests/fixtures/custom-thumbnail.jpg new file mode 100644 index 000000000..ef818442d --- /dev/null +++ b/server/tests/fixtures/custom-thumbnail.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/custom-thumbnail.png b/server/tests/fixtures/custom-thumbnail.png new file mode 100644 index 000000000..9f34daec1 --- /dev/null +++ b/server/tests/fixtures/custom-thumbnail.png | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/preview-big.png b/server/tests/fixtures/preview-big.png deleted file mode 100644 index 612e297f1..000000000 --- a/server/tests/fixtures/preview-big.png +++ /dev/null | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/preview.jpg b/server/tests/fixtures/preview.jpg deleted file mode 100644 index 1421da738..000000000 --- a/server/tests/fixtures/preview.jpg +++ /dev/null | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/thumbnail-big.jpg b/server/tests/fixtures/thumbnail-big.jpg deleted file mode 100644 index 537720d24..000000000 --- a/server/tests/fixtures/thumbnail-big.jpg +++ /dev/null | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/thumbnail.jpg b/server/tests/fixtures/thumbnail.jpg deleted file mode 100644 index 1e2897fb8..000000000 --- a/server/tests/fixtures/thumbnail.jpg +++ /dev/null | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/thumbnail.png b/server/tests/fixtures/thumbnail.png deleted file mode 100644 index b331aba3b..000000000 --- a/server/tests/fixtures/thumbnail.png +++ /dev/null | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short1-preview.webm.jpg b/server/tests/fixtures/video_short1-preview.webm.jpg index d65af1f21..15454942d 100644 --- a/server/tests/fixtures/video_short1-preview.webm.jpg +++ b/server/tests/fixtures/video_short1-preview.webm.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short1.webm.jpg b/server/tests/fixtures/video_short1.webm.jpg index 0ab7c58ad..b2740d73d 100644 --- a/server/tests/fixtures/video_short1.webm.jpg +++ b/server/tests/fixtures/video_short1.webm.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short2.webm.jpg b/server/tests/fixtures/video_short2.webm.jpg index 1e2897fb8..afe476c7f 100644 --- a/server/tests/fixtures/video_short2.webm.jpg +++ b/server/tests/fixtures/video_short2.webm.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_very_long_10p.mp4 b/server/tests/fixtures/video_very_long_10p.mp4 new file mode 100644 index 000000000..852297933 --- /dev/null +++ b/server/tests/fixtures/video_very_long_10p.mp4 | |||
Binary files differ | |||
diff --git a/server/tests/helpers/image.ts b/server/tests/helpers/image.ts index 530c9bacd..6021ffc48 100644 --- a/server/tests/helpers/image.ts +++ b/server/tests/helpers/image.ts | |||
@@ -35,28 +35,28 @@ describe('Image helpers', function () { | |||
35 | const thumbnailSize = { width: 280, height: 157 } | 35 | const thumbnailSize = { width: 280, height: 157 } |
36 | 36 | ||
37 | it('Should skip processing if the source image is okay', async function () { | 37 | it('Should skip processing if the source image is okay', async function () { |
38 | const input = buildAbsoluteFixturePath('thumbnail.jpg') | 38 | const input = buildAbsoluteFixturePath('custom-thumbnail.jpg') |
39 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) | 39 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) |
40 | 40 | ||
41 | await checkBuffers(input, imageDestJPG, true) | 41 | await checkBuffers(input, imageDestJPG, true) |
42 | }) | 42 | }) |
43 | 43 | ||
44 | it('Should not skip processing if the source image does not have the appropriate extension', async function () { | 44 | it('Should not skip processing if the source image does not have the appropriate extension', async function () { |
45 | const input = buildAbsoluteFixturePath('thumbnail.png') | 45 | const input = buildAbsoluteFixturePath('custom-thumbnail.png') |
46 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) | 46 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) |
47 | 47 | ||
48 | await checkBuffers(input, imageDestJPG, false) | 48 | await checkBuffers(input, imageDestJPG, false) |
49 | }) | 49 | }) |
50 | 50 | ||
51 | it('Should not skip processing if the source image does not have the appropriate size', async function () { | 51 | it('Should not skip processing if the source image does not have the appropriate size', async function () { |
52 | const input = buildAbsoluteFixturePath('preview.jpg') | 52 | const input = buildAbsoluteFixturePath('custom-preview.jpg') |
53 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) | 53 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) |
54 | 54 | ||
55 | await checkBuffers(input, imageDestJPG, false) | 55 | await checkBuffers(input, imageDestJPG, false) |
56 | }) | 56 | }) |
57 | 57 | ||
58 | it('Should not skip processing if the source image does not have the appropriate size', async function () { | 58 | it('Should not skip processing if the source image does not have the appropriate size', async function () { |
59 | const input = buildAbsoluteFixturePath('thumbnail-big.jpg') | 59 | const input = buildAbsoluteFixturePath('custom-thumbnail-big.jpg') |
60 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) | 60 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) |
61 | 61 | ||
62 | await checkBuffers(input, imageDestJPG, false) | 62 | await checkBuffers(input, imageDestJPG, false) |
diff --git a/server/tests/peertube-runner/studio-transcoding.ts b/server/tests/peertube-runner/studio-transcoding.ts index 988201947..c265d7934 100644 --- a/server/tests/peertube-runner/studio-transcoding.ts +++ b/server/tests/peertube-runner/studio-transcoding.ts | |||
@@ -44,8 +44,8 @@ describe('Test studio transcoding in peertube-runner program', function () { | |||
44 | } | 44 | } |
45 | 45 | ||
46 | if (objectStorage) { | 46 | if (objectStorage) { |
47 | for (const webtorrentFile of video.files) { | 47 | for (const webVideoFile of video.files) { |
48 | expectStartWith(webtorrentFile.fileUrl, objectStorage.getMockWebVideosBaseUrl()) | 48 | expectStartWith(webVideoFile.fileUrl, objectStorage.getMockWebVideosBaseUrl()) |
49 | } | 49 | } |
50 | 50 | ||
51 | for (const hlsFile of video.streamingPlaylists[0].files) { | 51 | for (const hlsFile of video.streamingPlaylists[0].files) { |
diff --git a/server/tests/peertube-runner/vod-transcoding.ts b/server/tests/peertube-runner/vod-transcoding.ts index c3f41c097..eef6faf4e 100644 --- a/server/tests/peertube-runner/vod-transcoding.ts +++ b/server/tests/peertube-runner/vod-transcoding.ts | |||
@@ -24,13 +24,13 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
24 | let peertubeRunner: PeerTubeRunnerProcess | 24 | let peertubeRunner: PeerTubeRunnerProcess |
25 | 25 | ||
26 | function runSuite (options: { | 26 | function runSuite (options: { |
27 | webtorrentEnabled: boolean | 27 | webVideoEnabled: boolean |
28 | hlsEnabled: boolean | 28 | hlsEnabled: boolean |
29 | objectStorage?: ObjectStorageCommand | 29 | objectStorage?: ObjectStorageCommand |
30 | }) { | 30 | }) { |
31 | const { webtorrentEnabled, hlsEnabled, objectStorage } = options | 31 | const { webVideoEnabled, hlsEnabled, objectStorage } = options |
32 | 32 | ||
33 | const objectStorageBaseUrlWebTorrent = objectStorage | 33 | const objectStorageBaseUrlWebVideo = objectStorage |
34 | ? objectStorage.getMockWebVideosBaseUrl() | 34 | ? objectStorage.getMockWebVideosBaseUrl() |
35 | : undefined | 35 | : undefined |
36 | 36 | ||
@@ -46,13 +46,13 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
46 | await waitJobs(servers, { runnerJobs: true }) | 46 | await waitJobs(servers, { runnerJobs: true }) |
47 | 47 | ||
48 | for (const server of servers) { | 48 | for (const server of servers) { |
49 | if (webtorrentEnabled) { | 49 | if (webVideoEnabled) { |
50 | await completeWebVideoFilesCheck({ | 50 | await completeWebVideoFilesCheck({ |
51 | server, | 51 | server, |
52 | originServer: servers[0], | 52 | originServer: servers[0], |
53 | fixture: 'video_short.mp4', | 53 | fixture: 'video_short.mp4', |
54 | videoUUID: uuid, | 54 | videoUUID: uuid, |
55 | objectStorageBaseUrl: objectStorageBaseUrlWebTorrent, | 55 | objectStorageBaseUrl: objectStorageBaseUrlWebVideo, |
56 | files: [ | 56 | files: [ |
57 | { resolution: 0 }, | 57 | { resolution: 0 }, |
58 | { resolution: 144 }, | 58 | { resolution: 144 }, |
@@ -66,7 +66,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
66 | 66 | ||
67 | if (hlsEnabled) { | 67 | if (hlsEnabled) { |
68 | await completeCheckHlsPlaylist({ | 68 | await completeCheckHlsPlaylist({ |
69 | hlsOnly: !webtorrentEnabled, | 69 | hlsOnly: !webVideoEnabled, |
70 | servers, | 70 | servers, |
71 | videoUUID: uuid, | 71 | videoUUID: uuid, |
72 | objectStorageBaseUrl: objectStorageBaseUrlHLS, | 72 | objectStorageBaseUrl: objectStorageBaseUrlHLS, |
@@ -84,13 +84,13 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
84 | await waitJobs(servers, { runnerJobs: true }) | 84 | await waitJobs(servers, { runnerJobs: true }) |
85 | 85 | ||
86 | for (const server of servers) { | 86 | for (const server of servers) { |
87 | if (webtorrentEnabled) { | 87 | if (webVideoEnabled) { |
88 | await completeWebVideoFilesCheck({ | 88 | await completeWebVideoFilesCheck({ |
89 | server, | 89 | server, |
90 | originServer: servers[0], | 90 | originServer: servers[0], |
91 | fixture: 'video_short.webm', | 91 | fixture: 'video_short.webm', |
92 | videoUUID: uuid, | 92 | videoUUID: uuid, |
93 | objectStorageBaseUrl: objectStorageBaseUrlWebTorrent, | 93 | objectStorageBaseUrl: objectStorageBaseUrlWebVideo, |
94 | files: [ | 94 | files: [ |
95 | { resolution: 0 }, | 95 | { resolution: 0 }, |
96 | { resolution: 144 }, | 96 | { resolution: 144 }, |
@@ -104,7 +104,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
104 | 104 | ||
105 | if (hlsEnabled) { | 105 | if (hlsEnabled) { |
106 | await completeCheckHlsPlaylist({ | 106 | await completeCheckHlsPlaylist({ |
107 | hlsOnly: !webtorrentEnabled, | 107 | hlsOnly: !webVideoEnabled, |
108 | servers, | 108 | servers, |
109 | videoUUID: uuid, | 109 | videoUUID: uuid, |
110 | objectStorageBaseUrl: objectStorageBaseUrlHLS, | 110 | objectStorageBaseUrl: objectStorageBaseUrlHLS, |
@@ -123,13 +123,13 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
123 | await waitJobs(servers, { runnerJobs: true }) | 123 | await waitJobs(servers, { runnerJobs: true }) |
124 | 124 | ||
125 | for (const server of servers) { | 125 | for (const server of servers) { |
126 | if (webtorrentEnabled) { | 126 | if (webVideoEnabled) { |
127 | await completeWebVideoFilesCheck({ | 127 | await completeWebVideoFilesCheck({ |
128 | server, | 128 | server, |
129 | originServer: servers[0], | 129 | originServer: servers[0], |
130 | fixture: 'sample.ogg', | 130 | fixture: 'sample.ogg', |
131 | videoUUID: uuid, | 131 | videoUUID: uuid, |
132 | objectStorageBaseUrl: objectStorageBaseUrlWebTorrent, | 132 | objectStorageBaseUrl: objectStorageBaseUrlWebVideo, |
133 | files: [ | 133 | files: [ |
134 | { resolution: 0 }, | 134 | { resolution: 0 }, |
135 | { resolution: 144 }, | 135 | { resolution: 144 }, |
@@ -142,7 +142,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
142 | 142 | ||
143 | if (hlsEnabled) { | 143 | if (hlsEnabled) { |
144 | await completeCheckHlsPlaylist({ | 144 | await completeCheckHlsPlaylist({ |
145 | hlsOnly: !webtorrentEnabled, | 145 | hlsOnly: !webVideoEnabled, |
146 | servers, | 146 | servers, |
147 | videoUUID: uuid, | 147 | videoUUID: uuid, |
148 | objectStorageBaseUrl: objectStorageBaseUrlHLS, | 148 | objectStorageBaseUrl: objectStorageBaseUrlHLS, |
@@ -159,13 +159,13 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
159 | 159 | ||
160 | await waitJobs(servers, { runnerJobs: true }) | 160 | await waitJobs(servers, { runnerJobs: true }) |
161 | 161 | ||
162 | if (webtorrentEnabled) { | 162 | if (webVideoEnabled) { |
163 | await completeWebVideoFilesCheck({ | 163 | await completeWebVideoFilesCheck({ |
164 | server: servers[0], | 164 | server: servers[0], |
165 | originServer: servers[0], | 165 | originServer: servers[0], |
166 | fixture: 'video_short.mp4', | 166 | fixture: 'video_short.mp4', |
167 | videoUUID: uuid, | 167 | videoUUID: uuid, |
168 | objectStorageBaseUrl: objectStorageBaseUrlWebTorrent, | 168 | objectStorageBaseUrl: objectStorageBaseUrlWebVideo, |
169 | files: [ | 169 | files: [ |
170 | { resolution: 0 }, | 170 | { resolution: 0 }, |
171 | { resolution: 144 }, | 171 | { resolution: 144 }, |
@@ -179,7 +179,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
179 | 179 | ||
180 | if (hlsEnabled) { | 180 | if (hlsEnabled) { |
181 | await completeCheckHlsPlaylist({ | 181 | await completeCheckHlsPlaylist({ |
182 | hlsOnly: !webtorrentEnabled, | 182 | hlsOnly: !webVideoEnabled, |
183 | servers: [ servers[0] ], | 183 | servers: [ servers[0] ], |
184 | videoUUID: uuid, | 184 | videoUUID: uuid, |
185 | objectStorageBaseUrl: objectStorageBaseUrlHLS, | 185 | objectStorageBaseUrl: objectStorageBaseUrlHLS, |
@@ -203,7 +203,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
203 | 203 | ||
204 | await servers[0].config.enableTranscoding(true, true, true) | 204 | await servers[0].config.enableTranscoding(true, true, true) |
205 | 205 | ||
206 | await servers[0].videos.runTranscoding({ transcodingType: 'webtorrent', videoId: uuid }) | 206 | await servers[0].videos.runTranscoding({ transcodingType: 'web-video', videoId: uuid }) |
207 | await waitJobs(servers, { runnerJobs: true }) | 207 | await waitJobs(servers, { runnerJobs: true }) |
208 | 208 | ||
209 | await completeWebVideoFilesCheck({ | 209 | await completeWebVideoFilesCheck({ |
@@ -211,7 +211,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
211 | originServer: servers[0], | 211 | originServer: servers[0], |
212 | fixture: 'video_short.mp4', | 212 | fixture: 'video_short.mp4', |
213 | videoUUID: uuid, | 213 | videoUUID: uuid, |
214 | objectStorageBaseUrl: objectStorageBaseUrlWebTorrent, | 214 | objectStorageBaseUrl: objectStorageBaseUrlWebVideo, |
215 | files: [ | 215 | files: [ |
216 | { resolution: 0 }, | 216 | { resolution: 0 }, |
217 | { resolution: 144 }, | 217 | { resolution: 144 }, |
@@ -262,7 +262,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
262 | await servers[0].config.enableTranscoding(true, false, true) | 262 | await servers[0].config.enableTranscoding(true, false, true) |
263 | }) | 263 | }) |
264 | 264 | ||
265 | runSuite({ webtorrentEnabled: true, hlsEnabled: false }) | 265 | runSuite({ webVideoEnabled: true, hlsEnabled: false }) |
266 | }) | 266 | }) |
267 | 267 | ||
268 | describe('HLS videos only enabled', function () { | 268 | describe('HLS videos only enabled', function () { |
@@ -271,7 +271,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
271 | await servers[0].config.enableTranscoding(false, true, true) | 271 | await servers[0].config.enableTranscoding(false, true, true) |
272 | }) | 272 | }) |
273 | 273 | ||
274 | runSuite({ webtorrentEnabled: false, hlsEnabled: true }) | 274 | runSuite({ webVideoEnabled: false, hlsEnabled: true }) |
275 | }) | 275 | }) |
276 | 276 | ||
277 | describe('Web video & HLS enabled', function () { | 277 | describe('Web video & HLS enabled', function () { |
@@ -280,7 +280,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
280 | await servers[0].config.enableTranscoding(true, true, true) | 280 | await servers[0].config.enableTranscoding(true, true, true) |
281 | }) | 281 | }) |
282 | 282 | ||
283 | runSuite({ webtorrentEnabled: true, hlsEnabled: true }) | 283 | runSuite({ webVideoEnabled: true, hlsEnabled: true }) |
284 | }) | 284 | }) |
285 | }) | 285 | }) |
286 | 286 | ||
@@ -306,7 +306,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
306 | await servers[0].config.enableTranscoding(true, false, true) | 306 | await servers[0].config.enableTranscoding(true, false, true) |
307 | }) | 307 | }) |
308 | 308 | ||
309 | runSuite({ webtorrentEnabled: true, hlsEnabled: false, objectStorage }) | 309 | runSuite({ webVideoEnabled: true, hlsEnabled: false, objectStorage }) |
310 | }) | 310 | }) |
311 | 311 | ||
312 | describe('HLS videos only enabled', function () { | 312 | describe('HLS videos only enabled', function () { |
@@ -315,7 +315,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
315 | await servers[0].config.enableTranscoding(false, true, true) | 315 | await servers[0].config.enableTranscoding(false, true, true) |
316 | }) | 316 | }) |
317 | 317 | ||
318 | runSuite({ webtorrentEnabled: false, hlsEnabled: true, objectStorage }) | 318 | runSuite({ webVideoEnabled: false, hlsEnabled: true, objectStorage }) |
319 | }) | 319 | }) |
320 | 320 | ||
321 | describe('Web video & HLS enabled', function () { | 321 | describe('Web video & HLS enabled', function () { |
@@ -324,7 +324,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
324 | await servers[0].config.enableTranscoding(true, true, true) | 324 | await servers[0].config.enableTranscoding(true, true, true) |
325 | }) | 325 | }) |
326 | 326 | ||
327 | runSuite({ webtorrentEnabled: true, hlsEnabled: true, objectStorage }) | 327 | runSuite({ webVideoEnabled: true, hlsEnabled: true, objectStorage }) |
328 | }) | 328 | }) |
329 | 329 | ||
330 | after(async function () { | 330 | after(async function () { |
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts index a02a53c50..a75a8c8fa 100644 --- a/server/tests/plugins/filter-hooks.ts +++ b/server/tests/plugins/filter-hooks.ts | |||
@@ -493,7 +493,7 @@ describe('Test plugin filter hooks', function () { | |||
493 | await servers[0].config.updateCustomSubConfig({ | 493 | await servers[0].config.updateCustomSubConfig({ |
494 | newConfig: { | 494 | newConfig: { |
495 | transcoding: { | 495 | transcoding: { |
496 | webtorrent: { | 496 | webVideos: { |
497 | enabled: true | 497 | enabled: true |
498 | }, | 498 | }, |
499 | hls: { | 499 | hls: { |
diff --git a/server/tests/plugins/plugin-helpers.ts b/server/tests/plugins/plugin-helpers.ts index e951a1299..f5a0cbe85 100644 --- a/server/tests/plugins/plugin-helpers.ts +++ b/server/tests/plugins/plugin-helpers.ts | |||
@@ -302,11 +302,11 @@ describe('Test plugin helpers', function () { | |||
302 | 302 | ||
303 | // Video files check | 303 | // Video files check |
304 | { | 304 | { |
305 | expect(body.webtorrent.videoFiles).to.be.an('array') | 305 | expect(body.webVideo.videoFiles).to.be.an('array') |
306 | expect(body.hls.videoFiles).to.be.an('array') | 306 | expect(body.hls.videoFiles).to.be.an('array') |
307 | 307 | ||
308 | for (const resolution of [ 144, 240, 360, 480, 720 ]) { | 308 | for (const resolution of [ 144, 240, 360, 480, 720 ]) { |
309 | for (const files of [ body.webtorrent.videoFiles, body.hls.videoFiles ]) { | 309 | for (const files of [ body.webVideo.videoFiles, body.hls.videoFiles ]) { |
310 | const file = files.find(f => f.resolution === resolution) | 310 | const file = files.find(f => f.resolution === resolution) |
311 | expect(file).to.exist | 311 | expect(file).to.exist |
312 | 312 | ||
@@ -318,7 +318,7 @@ describe('Test plugin helpers', function () { | |||
318 | } | 318 | } |
319 | } | 319 | } |
320 | 320 | ||
321 | videoPath = body.webtorrent.videoFiles[0].path | 321 | videoPath = body.webVideo.videoFiles[0].path |
322 | } | 322 | } |
323 | 323 | ||
324 | // Thumbnails check | 324 | // Thumbnails check |
diff --git a/server/tests/plugins/plugin-transcoding.ts b/server/tests/plugins/plugin-transcoding.ts index 689eec5ac..21f82fbac 100644 --- a/server/tests/plugins/plugin-transcoding.ts +++ b/server/tests/plugins/plugin-transcoding.ts | |||
@@ -35,7 +35,7 @@ function updateConf (server: PeerTubeServer, vodProfile: string, liveProfile: st | |||
35 | hls: { | 35 | hls: { |
36 | enabled: true | 36 | enabled: true |
37 | }, | 37 | }, |
38 | webtorrent: { | 38 | webVideos: { |
39 | enabled: true | 39 | enabled: true |
40 | }, | 40 | }, |
41 | resolutions: { | 41 | resolutions: { |
@@ -247,7 +247,7 @@ describe('Test transcoding plugins', function () { | |||
247 | 247 | ||
248 | const video = await server.videos.get({ id: videoUUID }) | 248 | const video = await server.videos.get({ id: videoUUID }) |
249 | 249 | ||
250 | const path = server.servers.buildWebTorrentFilePath(video.files[0].fileUrl) | 250 | const path = server.servers.buildWebVideoFilePath(video.files[0].fileUrl) |
251 | const audioProbe = await getAudioStream(path) | 251 | const audioProbe = await getAudioStream(path) |
252 | expect(audioProbe.audioStream.codec_name).to.equal('opus') | 252 | expect(audioProbe.audioStream.codec_name).to.equal('opus') |
253 | 253 | ||
diff --git a/server/tests/shared/checks.ts b/server/tests/shared/checks.ts index feaef37c6..90179c6ac 100644 --- a/server/tests/shared/checks.ts +++ b/server/tests/shared/checks.ts | |||
@@ -61,6 +61,16 @@ async function testImageSize (url: string, imageName: string, imageHTTPPath: str | |||
61 | expect(body.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture') | 61 | expect(body.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture') |
62 | } | 62 | } |
63 | 63 | ||
64 | async function testImageGeneratedByFFmpeg (url: string, imageName: string, imageHTTPPath: string, extension = '.jpg') { | ||
65 | if (process.env.ENABLE_FFMPEG_THUMBNAIL_PIXEL_COMPARISON_TESTS !== 'true') { | ||
66 | console.log( | ||
67 | 'Pixel comparison of image generated by ffmpeg is disabled. ' + | ||
68 | 'You can enable it using `ENABLE_FFMPEG_THUMBNAIL_PIXEL_COMPARISON_TESTS=true env variable') | ||
69 | } | ||
70 | |||
71 | return testImage(url, imageName, imageHTTPPath, extension) | ||
72 | } | ||
73 | |||
64 | async function testImage (url: string, imageName: string, imageHTTPPath: string, extension = '.jpg') { | 74 | async function testImage (url: string, imageName: string, imageHTTPPath: string, extension = '.jpg') { |
65 | const res = await makeGetRequest({ | 75 | const res = await makeGetRequest({ |
66 | url, | 76 | url, |
@@ -148,6 +158,7 @@ async function checkVideoDuration (server: PeerTubeServer, videoUUID: string, du | |||
148 | 158 | ||
149 | export { | 159 | export { |
150 | dateIsValid, | 160 | dateIsValid, |
161 | testImageGeneratedByFFmpeg, | ||
151 | testImageSize, | 162 | testImageSize, |
152 | testImage, | 163 | testImage, |
153 | expectLogDoesNotContain, | 164 | expectLogDoesNotContain, |
diff --git a/server/tests/shared/videos.ts b/server/tests/shared/videos.ts index 856fabd11..e09bd60b5 100644 --- a/server/tests/shared/videos.ts +++ b/server/tests/shared/videos.ts | |||
@@ -7,7 +7,7 @@ import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO | |||
7 | import { getLowercaseExtension, pick, uuidRegex } from '@shared/core-utils' | 7 | import { getLowercaseExtension, pick, uuidRegex } from '@shared/core-utils' |
8 | import { HttpStatusCode, VideoCaption, VideoDetails, VideoPrivacy, VideoResolution } from '@shared/models' | 8 | import { HttpStatusCode, VideoCaption, VideoDetails, VideoPrivacy, VideoResolution } from '@shared/models' |
9 | import { makeRawRequest, PeerTubeServer, VideoEdit, waitJobs } from '@shared/server-commands' | 9 | import { makeRawRequest, PeerTubeServer, VideoEdit, waitJobs } from '@shared/server-commands' |
10 | import { dateIsValid, expectStartWith, testImage } from './checks' | 10 | import { dateIsValid, expectStartWith, testImageGeneratedByFFmpeg } from './checks' |
11 | import { checkWebTorrentWorks } from './webtorrent' | 11 | import { checkWebTorrentWorks } from './webtorrent' |
12 | 12 | ||
13 | loadLanguages() | 13 | loadLanguages() |
@@ -28,7 +28,7 @@ async function completeWebVideoFilesCheck (options: { | |||
28 | const serverConfig = await originServer.config.getConfig() | 28 | const serverConfig = await originServer.config.getConfig() |
29 | const requiresAuth = video.privacy.id === VideoPrivacy.PRIVATE || video.privacy.id === VideoPrivacy.INTERNAL | 29 | const requiresAuth = video.privacy.id === VideoPrivacy.PRIVATE || video.privacy.id === VideoPrivacy.INTERNAL |
30 | 30 | ||
31 | const transcodingEnabled = serverConfig.transcoding.webtorrent.enabled | 31 | const transcodingEnabled = serverConfig.transcoding.web_videos.enabled |
32 | 32 | ||
33 | for (const attributeFile of files) { | 33 | for (const attributeFile of files) { |
34 | const file = video.files.find(f => f.resolution.id === attributeFile.resolution) | 34 | const file = video.files.find(f => f.resolution.id === attributeFile.resolution) |
@@ -51,11 +51,12 @@ async function completeWebVideoFilesCheck (options: { | |||
51 | expect(file.torrentUrl).to.match(new RegExp(`${server.url}/lazy-static/torrents/${nameReg}.torrent`)) | 51 | expect(file.torrentUrl).to.match(new RegExp(`${server.url}/lazy-static/torrents/${nameReg}.torrent`)) |
52 | 52 | ||
53 | if (objectStorageBaseUrl && requiresAuth) { | 53 | if (objectStorageBaseUrl && requiresAuth) { |
54 | expect(file.fileUrl).to.match(new RegExp(`${originServer.url}/object-storage-proxy/webseed/${privatePath}${nameReg}${extension}`)) | 54 | const regexp = new RegExp(`${originServer.url}/object-storage-proxy/web-videos/${privatePath}${nameReg}${extension}`) |
55 | expect(file.fileUrl).to.match(regexp) | ||
55 | } else if (objectStorageBaseUrl) { | 56 | } else if (objectStorageBaseUrl) { |
56 | expectStartWith(file.fileUrl, objectStorageBaseUrl) | 57 | expectStartWith(file.fileUrl, objectStorageBaseUrl) |
57 | } else { | 58 | } else { |
58 | expect(file.fileUrl).to.match(new RegExp(`${originServer.url}/static/webseed/${privatePath}${nameReg}${extension}`)) | 59 | expect(file.fileUrl).to.match(new RegExp(`${originServer.url}/static/web-videos/${privatePath}${nameReg}${extension}`)) |
59 | } | 60 | } |
60 | 61 | ||
61 | expect(file.fileDownloadUrl).to.match(new RegExp(`${originServer.url}/download/videos/${nameReg}${extension}`)) | 62 | expect(file.fileDownloadUrl).to.match(new RegExp(`${originServer.url}/download/videos/${nameReg}${extension}`)) |
@@ -197,11 +198,11 @@ async function completeVideoCheck (options: { | |||
197 | expect(video.downloadEnabled).to.equal(attributes.downloadEnabled) | 198 | expect(video.downloadEnabled).to.equal(attributes.downloadEnabled) |
198 | 199 | ||
199 | expect(video.thumbnailPath).to.exist | 200 | expect(video.thumbnailPath).to.exist |
200 | await testImage(server.url, attributes.thumbnailfile || attributes.fixture, video.thumbnailPath) | 201 | await testImageGeneratedByFFmpeg(server.url, attributes.thumbnailfile || attributes.fixture, video.thumbnailPath) |
201 | 202 | ||
202 | if (attributes.previewfile) { | 203 | if (attributes.previewfile) { |
203 | expect(video.previewPath).to.exist | 204 | expect(video.previewPath).to.exist |
204 | await testImage(server.url, attributes.previewfile, video.previewPath) | 205 | await testImageGeneratedByFFmpeg(server.url, attributes.previewfile, video.previewPath) |
205 | } | 206 | } |
206 | 207 | ||
207 | await completeWebVideoFilesCheck({ server, originServer, videoUUID: video.uuid, ...pick(attributes, [ 'fixture', 'files' ]) }) | 208 | await completeWebVideoFilesCheck({ server, originServer, videoUUID: video.uuid, ...pick(attributes, [ 'fixture', 'files' ]) }) |
@@ -215,22 +216,22 @@ async function checkVideoFilesWereRemoved (options: { | |||
215 | }) { | 216 | }) { |
216 | const { video, server, captions = [], onlyVideoFiles = false } = options | 217 | const { video, server, captions = [], onlyVideoFiles = false } = options |
217 | 218 | ||
218 | const webtorrentFiles = video.files || [] | 219 | const webVideoFiles = video.files || [] |
219 | const hlsFiles = video.streamingPlaylists[0]?.files || [] | 220 | const hlsFiles = video.streamingPlaylists[0]?.files || [] |
220 | 221 | ||
221 | const thumbnailName = basename(video.thumbnailPath) | 222 | const thumbnailName = basename(video.thumbnailPath) |
222 | const previewName = basename(video.previewPath) | 223 | const previewName = basename(video.previewPath) |
223 | 224 | ||
224 | const torrentNames = webtorrentFiles.concat(hlsFiles).map(f => basename(f.torrentUrl)) | 225 | const torrentNames = webVideoFiles.concat(hlsFiles).map(f => basename(f.torrentUrl)) |
225 | 226 | ||
226 | const captionNames = captions.map(c => basename(c.captionPath)) | 227 | const captionNames = captions.map(c => basename(c.captionPath)) |
227 | 228 | ||
228 | const webtorrentFilenames = webtorrentFiles.map(f => basename(f.fileUrl)) | 229 | const webVideoFilenames = webVideoFiles.map(f => basename(f.fileUrl)) |
229 | const hlsFilenames = hlsFiles.map(f => basename(f.fileUrl)) | 230 | const hlsFilenames = hlsFiles.map(f => basename(f.fileUrl)) |
230 | 231 | ||
231 | let directories: { [ directory: string ]: string[] } = { | 232 | let directories: { [ directory: string ]: string[] } = { |
232 | videos: webtorrentFilenames, | 233 | videos: webVideoFilenames, |
233 | redundancy: webtorrentFilenames, | 234 | redundancy: webVideoFilenames, |
234 | [join('playlists', 'hls')]: hlsFilenames, | 235 | [join('playlists', 'hls')]: hlsFilenames, |
235 | [join('redundancy', 'hls')]: hlsFilenames | 236 | [join('redundancy', 'hls')]: hlsFilenames |
236 | } | 237 | } |
diff --git a/server/tools/peertube-redundancy.ts b/server/tools/peertube-redundancy.ts index fd6c760b2..c24eb5233 100644 --- a/server/tools/peertube-redundancy.ts +++ b/server/tools/peertube-redundancy.ts | |||
@@ -65,19 +65,19 @@ async function listRedundanciesCLI (target: VideoRedundanciesTarget) { | |||
65 | }) as any | 65 | }) as any |
66 | 66 | ||
67 | for (const redundancy of data) { | 67 | for (const redundancy of data) { |
68 | const webtorrentFiles = redundancy.redundancies.files | 68 | const webVideoFiles = redundancy.redundancies.files |
69 | const streamingPlaylists = redundancy.redundancies.streamingPlaylists | 69 | const streamingPlaylists = redundancy.redundancies.streamingPlaylists |
70 | 70 | ||
71 | let totalSize = '' | 71 | let totalSize = '' |
72 | if (target === 'remote-videos') { | 72 | if (target === 'remote-videos') { |
73 | const tmp = webtorrentFiles.concat(streamingPlaylists) | 73 | const tmp = webVideoFiles.concat(streamingPlaylists) |
74 | .reduce((a, b) => a + b.size, 0) | 74 | .reduce((a, b) => a + b.size, 0) |
75 | 75 | ||
76 | totalSize = bytes(tmp) | 76 | totalSize = bytes(tmp) |
77 | } | 77 | } |
78 | 78 | ||
79 | const instances = uniqify( | 79 | const instances = uniqify( |
80 | webtorrentFiles.concat(streamingPlaylists) | 80 | webVideoFiles.concat(streamingPlaylists) |
81 | .map(r => r.fileUrl) | 81 | .map(r => r.fileUrl) |
82 | .map(u => new URL(u).host) | 82 | .map(u => new URL(u).host) |
83 | ) | 83 | ) |
@@ -86,7 +86,7 @@ async function listRedundanciesCLI (target: VideoRedundanciesTarget) { | |||
86 | redundancy.id.toString(), | 86 | redundancy.id.toString(), |
87 | redundancy.name, | 87 | redundancy.name, |
88 | redundancy.url, | 88 | redundancy.url, |
89 | webtorrentFiles.length, | 89 | webVideoFiles.length, |
90 | streamingPlaylists.length, | 90 | streamingPlaylists.length, |
91 | instances.join('\n'), | 91 | instances.join('\n'), |
92 | totalSize | 92 | totalSize |
diff --git a/server/types/express.d.ts b/server/types/express.d.ts index 510b9f94e..9c1be9bd1 100644 --- a/server/types/express.d.ts +++ b/server/types/express.d.ts | |||
@@ -18,6 +18,7 @@ import { | |||
18 | MVideoId, | 18 | MVideoId, |
19 | MVideoImmutable, | 19 | MVideoImmutable, |
20 | MVideoLiveFormattable, | 20 | MVideoLiveFormattable, |
21 | MVideoPassword, | ||
21 | MVideoPlaylistFull, | 22 | MVideoPlaylistFull, |
22 | MVideoPlaylistFullSummary | 23 | MVideoPlaylistFullSummary |
23 | } from '@server/types/models' | 24 | } from '@server/types/models' |
@@ -165,6 +166,8 @@ declare module 'express' { | |||
165 | videoCommentFull?: MCommentOwnerVideoReply | 166 | videoCommentFull?: MCommentOwnerVideoReply |
166 | videoCommentThread?: MComment | 167 | videoCommentThread?: MComment |
167 | 168 | ||
169 | videoPassword?: MVideoPassword | ||
170 | |||
168 | follow?: MActorFollowActorsDefault | 171 | follow?: MActorFollowActorsDefault |
169 | subscription?: MActorFollowActorsDefaultSubscription | 172 | subscription?: MActorFollowActorsDefaultSubscription |
170 | 173 | ||
diff --git a/server/types/models/video/index.ts b/server/types/models/video/index.ts index 6e45fcc79..7f05db666 100644 --- a/server/types/models/video/index.ts +++ b/server/types/models/video/index.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | export * from './local-video-viewer-watch-section' | 1 | export * from './local-video-viewer-watch-section' |
2 | export * from './local-video-viewer-watch-section' | 2 | export * from './local-video-viewer-watch-section' |
3 | export * from './local-video-viewer' | 3 | export * from './local-video-viewer' |
4 | export * from './storyboard' | ||
4 | export * from './schedule-video-update' | 5 | export * from './schedule-video-update' |
5 | export * from './tag' | 6 | export * from './tag' |
6 | export * from './thumbnail' | 7 | export * from './thumbnail' |
@@ -16,6 +17,7 @@ export * from './video-import' | |||
16 | export * from './video-live-replay-setting' | 17 | export * from './video-live-replay-setting' |
17 | export * from './video-live-session' | 18 | export * from './video-live-session' |
18 | export * from './video-live' | 19 | export * from './video-live' |
20 | export * from './video-password' | ||
19 | export * from './video-playlist' | 21 | export * from './video-playlist' |
20 | export * from './video-playlist-element' | 22 | export * from './video-playlist-element' |
21 | export * from './video-rate' | 23 | export * from './video-rate' |
diff --git a/server/types/models/video/storyboard.ts b/server/types/models/video/storyboard.ts new file mode 100644 index 000000000..a0403d4f0 --- /dev/null +++ b/server/types/models/video/storyboard.ts | |||
@@ -0,0 +1,15 @@ | |||
1 | import { StoryboardModel } from '@server/models/video/storyboard' | ||
2 | import { PickWith } from '@shared/typescript-utils' | ||
3 | import { MVideo } from './video' | ||
4 | |||
5 | type Use<K extends keyof StoryboardModel, M> = PickWith<StoryboardModel, K, M> | ||
6 | |||
7 | // ############################################################################ | ||
8 | |||
9 | export type MStoryboard = Omit<StoryboardModel, 'Video'> | ||
10 | |||
11 | // ############################################################################ | ||
12 | |||
13 | export type MStoryboardVideo = | ||
14 | MStoryboard & | ||
15 | Use<'Video', MVideo> | ||
diff --git a/server/types/models/video/video-caption.ts b/server/types/models/video/video-caption.ts index 8cd801064..d3adec362 100644 --- a/server/types/models/video/video-caption.ts +++ b/server/types/models/video/video-caption.ts | |||
@@ -11,7 +11,7 @@ export type MVideoCaption = Omit<VideoCaptionModel, 'Video'> | |||
11 | // ############################################################################ | 11 | // ############################################################################ |
12 | 12 | ||
13 | export type MVideoCaptionLanguage = Pick<MVideoCaption, 'language'> | 13 | export type MVideoCaptionLanguage = Pick<MVideoCaption, 'language'> |
14 | export type MVideoCaptionLanguageUrl = Pick<MVideoCaption, 'language' | 'fileUrl' | 'getFileUrl'> | 14 | export type MVideoCaptionLanguageUrl = Pick<MVideoCaption, 'language' | 'fileUrl' | 'filename' | 'getFileUrl' | 'getCaptionStaticPath'> |
15 | 15 | ||
16 | export type MVideoCaptionVideo = | 16 | export type MVideoCaptionVideo = |
17 | MVideoCaption & | 17 | MVideoCaption & |
diff --git a/server/types/models/video/video-file.ts b/server/types/models/video/video-file.ts index 55603e59c..68106788d 100644 --- a/server/types/models/video/video-file.ts +++ b/server/types/models/video/video-file.ts | |||
@@ -38,6 +38,6 @@ export function isStreamingPlaylistFile (file: any): file is MVideoFileStreaming | |||
38 | return !!file.videoStreamingPlaylistId | 38 | return !!file.videoStreamingPlaylistId |
39 | } | 39 | } |
40 | 40 | ||
41 | export function isWebtorrentFile (file: any): file is MVideoFileVideo { | 41 | export function isWebVideoFile (file: any): file is MVideoFileVideo { |
42 | return !!file.videoId | 42 | return !!file.videoId |
43 | } | 43 | } |
diff --git a/server/types/models/video/video-password.ts b/server/types/models/video/video-password.ts new file mode 100644 index 000000000..313cc3e0c --- /dev/null +++ b/server/types/models/video/video-password.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
2 | |||
3 | export type MVideoPassword = Omit<VideoPasswordModel, 'Video'> | ||
diff --git a/server/types/models/video/video.ts b/server/types/models/video/video.ts index 58ae7baad..53ee94269 100644 --- a/server/types/models/video/video.ts +++ b/server/types/models/video/video.ts | |||
@@ -3,6 +3,7 @@ import { VideoModel } from '../../../models/video/video' | |||
3 | import { MTrackerUrl } from '../server/tracker' | 3 | import { MTrackerUrl } from '../server/tracker' |
4 | import { MUserVideoHistoryTime } from '../user/user-video-history' | 4 | import { MUserVideoHistoryTime } from '../user/user-video-history' |
5 | import { MScheduleVideoUpdate } from './schedule-video-update' | 5 | import { MScheduleVideoUpdate } from './schedule-video-update' |
6 | import { MStoryboard } from './storyboard' | ||
6 | import { MTag } from './tag' | 7 | import { MTag } from './tag' |
7 | import { MThumbnail } from './thumbnail' | 8 | import { MThumbnail } from './thumbnail' |
8 | import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist' | 9 | import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist' |
@@ -32,7 +33,7 @@ type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M> | |||
32 | export type MVideo = | 33 | export type MVideo = |
33 | Omit<VideoModel, 'VideoChannel' | 'Tags' | 'Thumbnails' | 'VideoPlaylistElements' | 'VideoAbuses' | | 34 | Omit<VideoModel, 'VideoChannel' | 'Tags' | 'Thumbnails' | 'VideoPlaylistElements' | 'VideoAbuses' | |
34 | 'VideoFiles' | 'VideoStreamingPlaylists' | 'VideoShares' | 'AccountVideoRates' | 'VideoComments' | 'VideoViews' | 'UserVideoHistories' | | 35 | 'VideoFiles' | 'VideoStreamingPlaylists' | 'VideoShares' | 'AccountVideoRates' | 'VideoComments' | 'VideoViews' | 'UserVideoHistories' | |
35 | 'ScheduleVideoUpdate' | 'VideoBlacklist' | 'VideoImport' | 'VideoCaptions' | 'VideoLive' | 'Trackers'> | 36 | 'ScheduleVideoUpdate' | 'VideoBlacklist' | 'VideoImport' | 'VideoCaptions' | 'VideoLive' | 'Trackers' | 'VideoPasswords' | 'Storyboard'> |
36 | 37 | ||
37 | // ############################################################################ | 38 | // ############################################################################ |
38 | 39 | ||
@@ -46,7 +47,7 @@ export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'> | |||
46 | 47 | ||
47 | // ############################################################################ | 48 | // ############################################################################ |
48 | 49 | ||
49 | // Video raw associations: schedules, video files, tags, thumbnails, captions, streaming playlists | 50 | // Video raw associations: schedules, video files, tags, thumbnails, captions, streaming playlists, passwords |
50 | 51 | ||
51 | // "With" to not confuse with the VideoFile model | 52 | // "With" to not confuse with the VideoFile model |
52 | export type MVideoWithFile = | 53 | export type MVideoWithFile = |
@@ -173,9 +174,10 @@ export type MVideoAP = | |||
173 | Use<'VideoBlacklist', MVideoBlacklistUnfederated> & | 174 | Use<'VideoBlacklist', MVideoBlacklistUnfederated> & |
174 | Use<'VideoFiles', MVideoFileRedundanciesOpt[]> & | 175 | Use<'VideoFiles', MVideoFileRedundanciesOpt[]> & |
175 | Use<'Thumbnails', MThumbnail[]> & | 176 | Use<'Thumbnails', MThumbnail[]> & |
176 | Use<'VideoLive', MVideoLive> | 177 | Use<'VideoLive', MVideoLive> & |
178 | Use<'Storyboard', MStoryboard> | ||
177 | 179 | ||
178 | export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'> | 180 | export type MVideoAPLight = Omit<MVideoAP, 'VideoCaptions' | 'Storyboard'> |
179 | 181 | ||
180 | export type MVideoDetails = | 182 | export type MVideoDetails = |
181 | MVideo & | 183 | MVideo & |
diff --git a/server/types/plugins/register-server-option.model.ts b/server/types/plugins/register-server-option.model.ts index df419fff4..103ef234b 100644 --- a/server/types/plugins/register-server-option.model.ts +++ b/server/types/plugins/register-server-option.model.ts | |||
@@ -41,7 +41,17 @@ export type PeerTubeHelpers = { | |||
41 | ffprobe: (path: string) => Promise<any> | 41 | ffprobe: (path: string) => Promise<any> |
42 | 42 | ||
43 | getFiles: (id: number | string) => Promise<{ | 43 | getFiles: (id: number | string) => Promise<{ |
44 | webtorrent: { | 44 | webtorrent: { // TODO: remove in v7 |
45 | videoFiles: { | ||
46 | path: string // Could be null if using remote storage | ||
47 | url: string | ||
48 | resolution: number | ||
49 | size: number | ||
50 | fps: number | ||
51 | }[] | ||
52 | } | ||
53 | |||
54 | webVideo: { | ||
45 | videoFiles: { | 55 | videoFiles: { |
46 | path: string // Could be null if using remote storage | 56 | path: string // Could be null if using remote storage |
47 | url: string | 57 | url: string |