diff options
105 files changed, 2918 insertions, 1297 deletions
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 9ae6f9f12..b3818c8de 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts | |||
@@ -20,12 +20,12 @@ import { | |||
20 | } from '@app/core' | 20 | } from '@app/core' |
21 | import { HooksService } from '@app/core/plugins/hooks.service' | 21 | import { HooksService } from '@app/core/plugins/hooks.service' |
22 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' | 22 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' |
23 | import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' | 23 | import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main' |
24 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' | 24 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' |
25 | import { LiveVideoService } from '@app/shared/shared-video-live' | 25 | import { LiveVideoService } from '@app/shared/shared-video-live' |
26 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | 26 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' |
27 | import { logger } from '@root-helpers/logger' | 27 | import { logger } from '@root-helpers/logger' |
28 | import { isP2PEnabled } from '@root-helpers/video' | 28 | import { isP2PEnabled, videoRequiresAuth } from '@root-helpers/video' |
29 | import { timeToInt } from '@shared/core-utils' | 29 | import { timeToInt } from '@shared/core-utils' |
30 | import { | 30 | import { |
31 | HTMLServerConfig, | 31 | HTMLServerConfig, |
@@ -78,6 +78,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
78 | private nextVideoUUID = '' | 78 | private nextVideoUUID = '' |
79 | private nextVideoTitle = '' | 79 | private nextVideoTitle = '' |
80 | 80 | ||
81 | private videoFileToken: string | ||
82 | |||
81 | private currentTime: number | 83 | private currentTime: number |
82 | 84 | ||
83 | private paramsSub: Subscription | 85 | private paramsSub: Subscription |
@@ -110,6 +112,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
110 | private pluginService: PluginService, | 112 | private pluginService: PluginService, |
111 | private peertubeSocket: PeerTubeSocket, | 113 | private peertubeSocket: PeerTubeSocket, |
112 | private screenService: ScreenService, | 114 | private screenService: ScreenService, |
115 | private videoFileTokenService: VideoFileTokenService, | ||
113 | private location: PlatformLocation, | 116 | private location: PlatformLocation, |
114 | @Inject(LOCALE_ID) private localeId: string | 117 | @Inject(LOCALE_ID) private localeId: string |
115 | ) { } | 118 | ) { } |
@@ -252,12 +255,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
252 | 'filter:api.video-watch.video.get.result' | 255 | 'filter:api.video-watch.video.get.result' |
253 | ) | 256 | ) |
254 | 257 | ||
255 | const videoAndLiveObs: Observable<{ video: VideoDetails, live?: LiveVideo }> = videoObs.pipe( | 258 | const videoAndLiveObs: Observable<{ video: VideoDetails, live?: LiveVideo, videoFileToken?: string }> = videoObs.pipe( |
256 | switchMap(video => { | 259 | switchMap(video => { |
257 | if (!video.isLive) return of({ video }) | 260 | if (!video.isLive) return of({ video, live: undefined }) |
258 | 261 | ||
259 | return this.liveVideoService.getVideoLive(video.uuid) | 262 | return this.liveVideoService.getVideoLive(video.uuid) |
260 | .pipe(map(live => ({ live, video }))) | 263 | .pipe(map(live => ({ live, video }))) |
264 | }), | ||
265 | |||
266 | switchMap(({ video, live }) => { | ||
267 | if (!videoRequiresAuth(video)) return of({ video, live, videoFileToken: undefined }) | ||
268 | |||
269 | return this.videoFileTokenService.getVideoFileToken(video.uuid) | ||
270 | .pipe(map(({ token }) => ({ video, live, videoFileToken: token }))) | ||
261 | }) | 271 | }) |
262 | ) | 272 | ) |
263 | 273 | ||
@@ -266,7 +276,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
266 | this.videoCaptionService.listCaptions(videoId), | 276 | this.videoCaptionService.listCaptions(videoId), |
267 | this.userService.getAnonymousOrLoggedUser() | 277 | this.userService.getAnonymousOrLoggedUser() |
268 | ]).subscribe({ | 278 | ]).subscribe({ |
269 | next: ([ { video, live }, captionsResult, loggedInOrAnonymousUser ]) => { | 279 | next: ([ { video, live, videoFileToken }, captionsResult, loggedInOrAnonymousUser ]) => { |
270 | const queryParams = this.route.snapshot.queryParams | 280 | const queryParams = this.route.snapshot.queryParams |
271 | 281 | ||
272 | const urlOptions = { | 282 | const urlOptions = { |
@@ -283,7 +293,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
283 | peertubeLink: false | 293 | peertubeLink: false |
284 | } | 294 | } |
285 | 295 | ||
286 | this.onVideoFetched({ video, live, videoCaptions: captionsResult.data, loggedInOrAnonymousUser, urlOptions }) | 296 | this.onVideoFetched({ video, live, videoCaptions: captionsResult.data, videoFileToken, loggedInOrAnonymousUser, urlOptions }) |
287 | .catch(err => this.handleGlobalError(err)) | 297 | .catch(err => this.handleGlobalError(err)) |
288 | }, | 298 | }, |
289 | 299 | ||
@@ -356,16 +366,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
356 | video: VideoDetails | 366 | video: VideoDetails |
357 | live: LiveVideo | 367 | live: LiveVideo |
358 | videoCaptions: VideoCaption[] | 368 | videoCaptions: VideoCaption[] |
369 | videoFileToken: string | ||
370 | |||
359 | urlOptions: URLOptions | 371 | urlOptions: URLOptions |
360 | loggedInOrAnonymousUser: User | 372 | loggedInOrAnonymousUser: User |
361 | }) { | 373 | }) { |
362 | const { video, live, videoCaptions, urlOptions, loggedInOrAnonymousUser } = options | 374 | const { video, live, videoCaptions, urlOptions, videoFileToken, loggedInOrAnonymousUser } = options |
363 | 375 | ||
364 | this.subscribeToLiveEventsIfNeeded(this.video, video) | 376 | this.subscribeToLiveEventsIfNeeded(this.video, video) |
365 | 377 | ||
366 | this.video = video | 378 | this.video = video |
367 | this.videoCaptions = videoCaptions | 379 | this.videoCaptions = videoCaptions |
368 | this.liveVideo = live | 380 | this.liveVideo = live |
381 | this.videoFileToken = videoFileToken | ||
369 | 382 | ||
370 | // Re init attributes | 383 | // Re init attributes |
371 | this.playerPlaceholderImgSrc = undefined | 384 | this.playerPlaceholderImgSrc = undefined |
@@ -414,6 +427,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
414 | video: this.video, | 427 | video: this.video, |
415 | videoCaptions: this.videoCaptions, | 428 | videoCaptions: this.videoCaptions, |
416 | liveVideo: this.liveVideo, | 429 | liveVideo: this.liveVideo, |
430 | videoFileToken: this.videoFileToken, | ||
417 | urlOptions, | 431 | urlOptions, |
418 | loggedInOrAnonymousUser, | 432 | loggedInOrAnonymousUser, |
419 | user: this.user | 433 | user: this.user |
@@ -561,11 +575,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
561 | video: VideoDetails | 575 | video: VideoDetails |
562 | liveVideo: LiveVideo | 576 | liveVideo: LiveVideo |
563 | videoCaptions: VideoCaption[] | 577 | videoCaptions: VideoCaption[] |
578 | |||
579 | videoFileToken: string | ||
580 | |||
564 | urlOptions: CustomizationOptions & { playerMode: PlayerMode } | 581 | urlOptions: CustomizationOptions & { playerMode: PlayerMode } |
582 | |||
565 | loggedInOrAnonymousUser: User | 583 | loggedInOrAnonymousUser: User |
566 | user?: AuthUser // Keep for plugins | 584 | user?: AuthUser // Keep for plugins |
567 | }) { | 585 | }) { |
568 | const { video, liveVideo, videoCaptions, urlOptions, loggedInOrAnonymousUser } = params | 586 | const { video, liveVideo, videoCaptions, videoFileToken, urlOptions, loggedInOrAnonymousUser } = params |
569 | 587 | ||
570 | const getStartTime = () => { | 588 | const getStartTime = () => { |
571 | const byUrl = urlOptions.startTime !== undefined | 589 | const byUrl = urlOptions.startTime !== undefined |
@@ -623,13 +641,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
623 | theaterButton: true, | 641 | theaterButton: true, |
624 | captions: videoCaptions.length !== 0, | 642 | captions: videoCaptions.length !== 0, |
625 | 643 | ||
626 | videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE | ||
627 | ? this.videoService.getVideoViewUrl(video.uuid) | ||
628 | : null, | ||
629 | authorizationHeader: this.authService.getRequestHeaderValue(), | ||
630 | |||
631 | metricsUrl: environment.apiUrl + '/api/v1/metrics/playback', | ||
632 | |||
633 | embedUrl: video.embedUrl, | 644 | embedUrl: video.embedUrl, |
634 | embedTitle: video.name, | 645 | embedTitle: video.name, |
635 | instanceName: this.serverConfig.instance.name, | 646 | instanceName: this.serverConfig.instance.name, |
@@ -639,7 +650,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
639 | 650 | ||
640 | language: this.localeId, | 651 | language: this.localeId, |
641 | 652 | ||
642 | serverUrl: environment.apiUrl, | 653 | metricsUrl: environment.apiUrl + '/api/v1/metrics/playback', |
654 | |||
655 | videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE | ||
656 | ? this.videoService.getVideoViewUrl(video.uuid) | ||
657 | : null, | ||
658 | authorizationHeader: () => this.authService.getRequestHeaderValue(), | ||
659 | |||
660 | serverUrl: environment.originServerUrl, | ||
661 | |||
662 | videoFileToken: () => videoFileToken, | ||
663 | requiresAuth: videoRequiresAuth(video), | ||
643 | 664 | ||
644 | videoCaptions: playerCaptions, | 665 | videoCaptions: playerCaptions, |
645 | 666 | ||
diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts index cd9665e37..a12325421 100644 --- a/client/src/app/core/auth/auth-user.model.ts +++ b/client/src/app/core/auth/auth-user.model.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Observable, of } from 'rxjs' | 1 | import { Observable, of } from 'rxjs' |
2 | import { map } from 'rxjs/operators' | 2 | import { map } from 'rxjs/operators' |
3 | import { User } from '@app/core/users/user.model' | 3 | import { User } from '@app/core/users/user.model' |
4 | import { UserTokens } from '@root-helpers/users' | 4 | import { OAuthUserTokens } from '@root-helpers/users' |
5 | import { hasUserRight } from '@shared/core-utils/users' | 5 | import { hasUserRight } from '@shared/core-utils/users' |
6 | import { | 6 | import { |
7 | MyUser as ServerMyUserModel, | 7 | MyUser as ServerMyUserModel, |
@@ -13,33 +13,33 @@ import { | |||
13 | } from '@shared/models' | 13 | } from '@shared/models' |
14 | 14 | ||
15 | export class AuthUser extends User implements ServerMyUserModel { | 15 | export class AuthUser extends User implements ServerMyUserModel { |
16 | tokens: UserTokens | 16 | oauthTokens: OAuthUserTokens |
17 | specialPlaylists: MyUserSpecialPlaylist[] | 17 | specialPlaylists: MyUserSpecialPlaylist[] |
18 | 18 | ||
19 | canSeeVideosLink = true | 19 | canSeeVideosLink = true |
20 | 20 | ||
21 | constructor (userHash: Partial<ServerMyUserModel>, hashTokens: Partial<UserTokens>) { | 21 | constructor (userHash: Partial<ServerMyUserModel>, hashTokens: Partial<OAuthUserTokens>) { |
22 | super(userHash) | 22 | super(userHash) |
23 | 23 | ||
24 | this.tokens = new UserTokens(hashTokens) | 24 | this.oauthTokens = new OAuthUserTokens(hashTokens) |
25 | this.specialPlaylists = userHash.specialPlaylists | 25 | this.specialPlaylists = userHash.specialPlaylists |
26 | } | 26 | } |
27 | 27 | ||
28 | getAccessToken () { | 28 | getAccessToken () { |
29 | return this.tokens.accessToken | 29 | return this.oauthTokens.accessToken |
30 | } | 30 | } |
31 | 31 | ||
32 | getRefreshToken () { | 32 | getRefreshToken () { |
33 | return this.tokens.refreshToken | 33 | return this.oauthTokens.refreshToken |
34 | } | 34 | } |
35 | 35 | ||
36 | getTokenType () { | 36 | getTokenType () { |
37 | return this.tokens.tokenType | 37 | return this.oauthTokens.tokenType |
38 | } | 38 | } |
39 | 39 | ||
40 | refreshTokens (accessToken: string, refreshToken: string) { | 40 | refreshTokens (accessToken: string, refreshToken: string) { |
41 | this.tokens.accessToken = accessToken | 41 | this.oauthTokens.accessToken = accessToken |
42 | this.tokens.refreshToken = refreshToken | 42 | this.oauthTokens.refreshToken = refreshToken |
43 | } | 43 | } |
44 | 44 | ||
45 | hasRight (right: UserRight) { | 45 | hasRight (right: UserRight) { |
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 7f4fae4aa..4de28e51e 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts | |||
@@ -5,7 +5,7 @@ import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular | |||
5 | import { Injectable } from '@angular/core' | 5 | import { Injectable } from '@angular/core' |
6 | import { Router } from '@angular/router' | 6 | import { Router } from '@angular/router' |
7 | import { Notifier } from '@app/core/notification/notifier.service' | 7 | import { Notifier } from '@app/core/notification/notifier.service' |
8 | import { logger, objectToUrlEncoded, peertubeLocalStorage, UserTokens } from '@root-helpers/index' | 8 | import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index' |
9 | import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' | 9 | import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' |
10 | import { environment } from '../../../environments/environment' | 10 | import { environment } from '../../../environments/environment' |
11 | import { RestExtractor } from '../rest/rest-extractor.service' | 11 | import { RestExtractor } from '../rest/rest-extractor.service' |
@@ -74,7 +74,7 @@ export class AuthService { | |||
74 | ] | 74 | ] |
75 | } | 75 | } |
76 | 76 | ||
77 | buildAuthUser (userInfo: Partial<User>, tokens: UserTokens) { | 77 | buildAuthUser (userInfo: Partial<User>, tokens: OAuthUserTokens) { |
78 | this.user = new AuthUser(userInfo, tokens) | 78 | this.user = new AuthUser(userInfo, tokens) |
79 | } | 79 | } |
80 | 80 | ||
diff --git a/client/src/app/core/users/user-local-storage.service.ts b/client/src/app/core/users/user-local-storage.service.ts index fff649eef..f1588bdd2 100644 --- a/client/src/app/core/users/user-local-storage.service.ts +++ b/client/src/app/core/users/user-local-storage.service.ts | |||
@@ -4,7 +4,7 @@ import { Injectable } from '@angular/core' | |||
4 | import { AuthService, AuthStatus } from '@app/core/auth' | 4 | import { AuthService, AuthStatus } from '@app/core/auth' |
5 | import { getBoolOrDefault } from '@root-helpers/local-storage-utils' | 5 | import { getBoolOrDefault } from '@root-helpers/local-storage-utils' |
6 | import { logger } from '@root-helpers/logger' | 6 | import { logger } from '@root-helpers/logger' |
7 | import { UserLocalStorageKeys, UserTokens } from '@root-helpers/users' | 7 | import { UserLocalStorageKeys, OAuthUserTokens } from '@root-helpers/users' |
8 | import { UserRole, UserUpdateMe } from '@shared/models' | 8 | import { UserRole, UserUpdateMe } from '@shared/models' |
9 | import { NSFWPolicyType } from '@shared/models/videos' | 9 | import { NSFWPolicyType } from '@shared/models/videos' |
10 | import { ServerService } from '../server' | 10 | import { ServerService } from '../server' |
@@ -24,7 +24,7 @@ export class UserLocalStorageService { | |||
24 | 24 | ||
25 | this.setLoggedInUser(user) | 25 | this.setLoggedInUser(user) |
26 | this.setUserInfo(user) | 26 | this.setUserInfo(user) |
27 | this.setTokens(user.tokens) | 27 | this.setTokens(user.oauthTokens) |
28 | } | 28 | } |
29 | }) | 29 | }) |
30 | 30 | ||
@@ -43,7 +43,7 @@ export class UserLocalStorageService { | |||
43 | next: () => { | 43 | next: () => { |
44 | const user = this.authService.getUser() | 44 | const user = this.authService.getUser() |
45 | 45 | ||
46 | this.setTokens(user.tokens) | 46 | this.setTokens(user.oauthTokens) |
47 | } | 47 | } |
48 | }) | 48 | }) |
49 | } | 49 | } |
@@ -174,14 +174,14 @@ export class UserLocalStorageService { | |||
174 | // --------------------------------------------------------------------------- | 174 | // --------------------------------------------------------------------------- |
175 | 175 | ||
176 | getTokens () { | 176 | getTokens () { |
177 | return UserTokens.getUserTokens(this.localStorageService) | 177 | return OAuthUserTokens.getUserTokens(this.localStorageService) |
178 | } | 178 | } |
179 | 179 | ||
180 | setTokens (tokens: UserTokens) { | 180 | setTokens (tokens: OAuthUserTokens) { |
181 | UserTokens.saveToLocalStorage(this.localStorageService, tokens) | 181 | OAuthUserTokens.saveToLocalStorage(this.localStorageService, tokens) |
182 | } | 182 | } |
183 | 183 | ||
184 | flushTokens () { | 184 | flushTokens () { |
185 | UserTokens.flushLocalStorage(this.localStorageService) | 185 | OAuthUserTokens.flushLocalStorage(this.localStorageService) |
186 | } | 186 | } |
187 | } | 187 | } |
diff --git a/client/src/app/helpers/utils/url.ts b/client/src/app/helpers/utils/url.ts index 08c27e3c1..9e7dc3e6f 100644 --- a/client/src/app/helpers/utils/url.ts +++ b/client/src/app/helpers/utils/url.ts | |||
@@ -54,8 +54,9 @@ function objectToFormData (obj: any, form?: FormData, namespace?: string) { | |||
54 | } | 54 | } |
55 | 55 | ||
56 | export { | 56 | export { |
57 | objectToFormData, | ||
58 | getAbsoluteAPIUrl, | 57 | getAbsoluteAPIUrl, |
59 | getAPIHost, | 58 | getAPIHost, |
60 | getAbsoluteEmbedUrl | 59 | getAbsoluteEmbedUrl, |
60 | |||
61 | objectToFormData | ||
61 | } | 62 | } |
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index 04b223cc5..c1523bc50 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -44,7 +44,15 @@ import { | |||
44 | import { PluginPlaceholderComponent, PluginSelectorDirective } from './plugins' | 44 | import { PluginPlaceholderComponent, PluginSelectorDirective } from './plugins' |
45 | import { ActorRedirectGuard } from './router' | 45 | import { ActorRedirectGuard } from './router' |
46 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' | 46 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' |
47 | import { EmbedComponent, RedundancyService, VideoImportService, VideoOwnershipService, VideoResolver, VideoService } from './video' | 47 | import { |
48 | EmbedComponent, | ||
49 | RedundancyService, | ||
50 | VideoFileTokenService, | ||
51 | VideoImportService, | ||
52 | VideoOwnershipService, | ||
53 | VideoResolver, | ||
54 | VideoService | ||
55 | } from './video' | ||
48 | import { VideoCaptionService } from './video-caption' | 56 | import { VideoCaptionService } from './video-caption' |
49 | import { VideoChannelService } from './video-channel' | 57 | import { VideoChannelService } from './video-channel' |
50 | 58 | ||
@@ -185,6 +193,7 @@ import { VideoChannelService } from './video-channel' | |||
185 | VideoImportService, | 193 | VideoImportService, |
186 | VideoOwnershipService, | 194 | VideoOwnershipService, |
187 | VideoService, | 195 | VideoService, |
196 | VideoFileTokenService, | ||
188 | VideoResolver, | 197 | VideoResolver, |
189 | 198 | ||
190 | VideoCaptionService, | 199 | VideoCaptionService, |
diff --git a/client/src/app/shared/shared-main/video/index.ts b/client/src/app/shared/shared-main/video/index.ts index 361601456..a2e47883e 100644 --- a/client/src/app/shared/shared-main/video/index.ts +++ b/client/src/app/shared/shared-main/video/index.ts | |||
@@ -2,6 +2,7 @@ export * from './embed.component' | |||
2 | export * from './redundancy.service' | 2 | export * from './redundancy.service' |
3 | export * from './video-details.model' | 3 | export * from './video-details.model' |
4 | export * from './video-edit.model' | 4 | export * from './video-edit.model' |
5 | export * from './video-file-token.service' | ||
5 | export * from './video-import.service' | 6 | export * from './video-import.service' |
6 | export * from './video-ownership.service' | 7 | export * from './video-ownership.service' |
7 | export * from './video.model' | 8 | export * from './video.model' |
diff --git a/client/src/app/shared/shared-main/video/video-file-token.service.ts b/client/src/app/shared/shared-main/video/video-file-token.service.ts new file mode 100644 index 000000000..791607249 --- /dev/null +++ b/client/src/app/shared/shared-main/video/video-file-token.service.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | import { catchError, map, of, tap } from 'rxjs' | ||
2 | import { HttpClient } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { RestExtractor } from '@app/core' | ||
5 | import { VideoToken } from '@shared/models' | ||
6 | import { VideoService } from './video.service' | ||
7 | |||
8 | @Injectable() | ||
9 | export class VideoFileTokenService { | ||
10 | |||
11 | private readonly store = new Map<string, { token: string, expires: Date }>() | ||
12 | |||
13 | constructor ( | ||
14 | private authHttp: HttpClient, | ||
15 | private restExtractor: RestExtractor | ||
16 | ) {} | ||
17 | |||
18 | getVideoFileToken (videoUUID: string) { | ||
19 | const existing = this.store.get(videoUUID) | ||
20 | if (existing) return of(existing) | ||
21 | |||
22 | return this.createVideoFileToken(videoUUID) | ||
23 | .pipe(tap(result => this.store.set(videoUUID, { token: result.token, expires: new Date(result.expires) }))) | ||
24 | } | ||
25 | |||
26 | private createVideoFileToken (videoUUID: string) { | ||
27 | return this.authHttp.post<VideoToken>(`${VideoService.BASE_VIDEO_URL}/${videoUUID}/token`, {}) | ||
28 | .pipe( | ||
29 | map(({ files }) => files), | ||
30 | catchError(err => this.restExtractor.handleError(err)) | ||
31 | ) | ||
32 | } | ||
33 | } | ||
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.html b/client/src/app/shared/shared-video-miniature/video-download.component.html index 1c7458b4b..1f622933d 100644 --- a/client/src/app/shared/shared-video-miniature/video-download.component.html +++ b/client/src/app/shared/shared-video-miniature/video-download.component.html | |||
@@ -48,10 +48,7 @@ | |||
48 | 48 | ||
49 | <ng-template ngbNavContent> | 49 | <ng-template ngbNavContent> |
50 | <div class="nav-content"> | 50 | <div class="nav-content"> |
51 | <my-input-text | 51 | <my-input-text [show]="true" [readonly]="true" [withCopy]="true" [withToggle]="false" [value]="getLink()"></my-input-text> |
52 | *ngIf="!isConfidentialVideo()" | ||
53 | [show]="true" [readonly]="true" [withCopy]="true" [withToggle]="false" [value]="getLink()" | ||
54 | ></my-input-text> | ||
55 | </div> | 52 | </div> |
56 | </ng-template> | 53 | </ng-template> |
57 | </ng-container> | 54 | </ng-container> |
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.ts b/client/src/app/shared/shared-video-miniature/video-download.component.ts index 47482caaa..667cb107f 100644 --- a/client/src/app/shared/shared-video-miniature/video-download.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-download.component.ts | |||
@@ -2,11 +2,12 @@ import { mapValues, pick } from 'lodash-es' | |||
2 | import { firstValueFrom } from 'rxjs' | 2 | import { firstValueFrom } from 'rxjs' |
3 | import { tap } from 'rxjs/operators' | 3 | import { tap } from 'rxjs/operators' |
4 | import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' | 4 | import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' |
5 | import { AuthService, HooksService, Notifier } from '@app/core' | 5 | import { HooksService } from '@app/core' |
6 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
7 | import { logger } from '@root-helpers/logger' | 7 | import { logger } from '@root-helpers/logger' |
8 | import { videoRequiresAuth } from '@root-helpers/video' | ||
8 | import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' | 9 | import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' |
9 | import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main' | 10 | import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoFileTokenService, VideoService } from '../shared-main' |
10 | 11 | ||
11 | type DownloadType = 'video' | 'subtitles' | 12 | type DownloadType = 'video' | 'subtitles' |
12 | type FileMetadata = { [key: string]: { label: string, value: string }} | 13 | type FileMetadata = { [key: string]: { label: string, value: string }} |
@@ -32,6 +33,8 @@ export class VideoDownloadComponent { | |||
32 | 33 | ||
33 | type: DownloadType = 'video' | 34 | type: DownloadType = 'video' |
34 | 35 | ||
36 | videoFileToken: string | ||
37 | |||
35 | private activeModal: NgbModalRef | 38 | private activeModal: NgbModalRef |
36 | 39 | ||
37 | private bytesPipe: BytesPipe | 40 | private bytesPipe: BytesPipe |
@@ -42,10 +45,9 @@ export class VideoDownloadComponent { | |||
42 | 45 | ||
43 | constructor ( | 46 | constructor ( |
44 | @Inject(LOCALE_ID) private localeId: string, | 47 | @Inject(LOCALE_ID) private localeId: string, |
45 | private notifier: Notifier, | ||
46 | private modalService: NgbModal, | 48 | private modalService: NgbModal, |
47 | private videoService: VideoService, | 49 | private videoService: VideoService, |
48 | private auth: AuthService, | 50 | private videoFileTokenService: VideoFileTokenService, |
49 | private hooks: HooksService | 51 | private hooks: HooksService |
50 | ) { | 52 | ) { |
51 | this.bytesPipe = new BytesPipe() | 53 | this.bytesPipe = new BytesPipe() |
@@ -71,6 +73,8 @@ export class VideoDownloadComponent { | |||
71 | } | 73 | } |
72 | 74 | ||
73 | show (video: VideoDetails, videoCaptions?: VideoCaption[]) { | 75 | show (video: VideoDetails, videoCaptions?: VideoCaption[]) { |
76 | this.videoFileToken = undefined | ||
77 | |||
74 | this.video = video | 78 | this.video = video |
75 | this.videoCaptions = videoCaptions | 79 | this.videoCaptions = videoCaptions |
76 | 80 | ||
@@ -84,6 +88,11 @@ export class VideoDownloadComponent { | |||
84 | this.subtitleLanguageId = this.videoCaptions[0].language.id | 88 | this.subtitleLanguageId = this.videoCaptions[0].language.id |
85 | } | 89 | } |
86 | 90 | ||
91 | if (videoRequiresAuth(this.video)) { | ||
92 | this.videoFileTokenService.getVideoFileToken(this.video.uuid) | ||
93 | .subscribe(({ token }) => this.videoFileToken = token) | ||
94 | } | ||
95 | |||
87 | this.activeModal.shown.subscribe(() => { | 96 | this.activeModal.shown.subscribe(() => { |
88 | this.hooks.runAction('action:modal.video-download.shown', 'common') | 97 | this.hooks.runAction('action:modal.video-download.shown', 'common') |
89 | }) | 98 | }) |
@@ -155,7 +164,7 @@ export class VideoDownloadComponent { | |||
155 | if (!file) return '' | 164 | if (!file) return '' |
156 | 165 | ||
157 | const suffix = this.isConfidentialVideo() | 166 | const suffix = this.isConfidentialVideo() |
158 | ? '?access_token=' + this.auth.getAccessToken() | 167 | ? '?videoFileToken=' + this.videoFileToken |
159 | : '' | 168 | : '' |
160 | 169 | ||
161 | switch (this.downloadType) { | 170 | switch (this.downloadType) { |
diff --git a/client/src/assets/player/shared/common/utils.ts b/client/src/assets/player/shared/common/utils.ts index a010d9184..609240626 100644 --- a/client/src/assets/player/shared/common/utils.ts +++ b/client/src/assets/player/shared/common/utils.ts | |||
@@ -52,6 +52,10 @@ function getRtcConfig () { | |||
52 | } | 52 | } |
53 | } | 53 | } |
54 | 54 | ||
55 | function isSameOrigin (current: string, target: string) { | ||
56 | return new URL(current).origin === new URL(target).origin | ||
57 | } | ||
58 | |||
55 | // --------------------------------------------------------------------------- | 59 | // --------------------------------------------------------------------------- |
56 | 60 | ||
57 | export { | 61 | export { |
@@ -60,5 +64,7 @@ export { | |||
60 | 64 | ||
61 | videoFileMaxByResolution, | 65 | videoFileMaxByResolution, |
62 | videoFileMinByResolution, | 66 | videoFileMinByResolution, |
63 | bytes | 67 | bytes, |
68 | |||
69 | isSameOrigin | ||
64 | } | 70 | } |
diff --git a/client/src/assets/player/shared/manager-options/hls-options-builder.ts b/client/src/assets/player/shared/manager-options/hls-options-builder.ts index 361c76f4b..933c0d595 100644 --- a/client/src/assets/player/shared/manager-options/hls-options-builder.ts +++ b/client/src/assets/player/shared/manager-options/hls-options-builder.ts | |||
@@ -5,7 +5,7 @@ import { LiveVideoLatencyMode } from '@shared/models' | |||
5 | import { getAverageBandwidthInStore } from '../../peertube-player-local-storage' | 5 | import { getAverageBandwidthInStore } from '../../peertube-player-local-storage' |
6 | import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../../types' | 6 | import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../../types' |
7 | import { PeertubePlayerManagerOptions } from '../../types/manager-options' | 7 | import { PeertubePlayerManagerOptions } from '../../types/manager-options' |
8 | import { getRtcConfig } from '../common' | 8 | import { getRtcConfig, isSameOrigin } from '../common' |
9 | import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager' | 9 | import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager' |
10 | import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder' | 10 | import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder' |
11 | import { segmentValidatorFactory } from '../p2p-media-loader/segment-validator' | 11 | import { segmentValidatorFactory } from '../p2p-media-loader/segment-validator' |
@@ -84,7 +84,21 @@ export class HLSOptionsBuilder { | |||
84 | simultaneousHttpDownloads: 1, | 84 | simultaneousHttpDownloads: 1, |
85 | httpFailedSegmentTimeout: 1000, | 85 | httpFailedSegmentTimeout: 1000, |
86 | 86 | ||
87 | segmentValidator: segmentValidatorFactory(this.options.p2pMediaLoader.segmentsSha256Url, this.options.common.isLive), | 87 | xhrSetup: (xhr, url) => { |
88 | if (!this.options.common.requiresAuth) return | ||
89 | if (!isSameOrigin(this.options.common.serverUrl, url)) return | ||
90 | |||
91 | xhr.setRequestHeader('Authorization', this.options.common.authorizationHeader()) | ||
92 | }, | ||
93 | |||
94 | segmentValidator: segmentValidatorFactory({ | ||
95 | segmentsSha256Url: this.options.p2pMediaLoader.segmentsSha256Url, | ||
96 | isLive: this.options.common.isLive, | ||
97 | authorizationHeader: this.options.common.authorizationHeader, | ||
98 | requiresAuth: this.options.common.requiresAuth, | ||
99 | serverUrl: this.options.common.serverUrl | ||
100 | }), | ||
101 | |||
88 | segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager), | 102 | segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager), |
89 | 103 | ||
90 | useP2P: this.options.common.p2pEnabled, | 104 | useP2P: this.options.common.p2pEnabled, |
diff --git a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts b/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts index 257cf1e05..b5bdcd4e6 100644 --- a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts +++ b/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { PeertubePlayerManagerOptions } from '../../types' | 1 | import { addQueryParams } from '../../../../../../shared/core-utils' |
2 | import { PeertubePlayerManagerOptions, WebtorrentPluginOptions } from '../../types' | ||
2 | 3 | ||
3 | export class WebTorrentOptionsBuilder { | 4 | export class WebTorrentOptionsBuilder { |
4 | 5 | ||
@@ -16,13 +17,23 @@ export class WebTorrentOptionsBuilder { | |||
16 | 17 | ||
17 | const autoplay = this.autoPlayValue === 'play' | 18 | const autoplay = this.autoPlayValue === 'play' |
18 | 19 | ||
19 | const webtorrent = { | 20 | const webtorrent: WebtorrentPluginOptions = { |
20 | autoplay, | 21 | autoplay, |
21 | 22 | ||
22 | playerRefusedP2P: commonOptions.p2pEnabled === false, | 23 | playerRefusedP2P: commonOptions.p2pEnabled === false, |
23 | videoDuration: commonOptions.videoDuration, | 24 | videoDuration: commonOptions.videoDuration, |
24 | playerElement: commonOptions.playerElement, | 25 | playerElement: commonOptions.playerElement, |
25 | 26 | ||
27 | videoFileToken: commonOptions.videoFileToken, | ||
28 | |||
29 | requiresAuth: commonOptions.requiresAuth, | ||
30 | |||
31 | buildWebSeedUrls: file => { | ||
32 | if (!commonOptions.requiresAuth) return [] | ||
33 | |||
34 | return [ addQueryParams(file.fileUrl, { videoFileToken: commonOptions.videoFileToken() }) ] | ||
35 | }, | ||
36 | |||
26 | videoFiles: webtorrentOptions.videoFiles.length !== 0 | 37 | videoFiles: webtorrentOptions.videoFiles.length !== 0 |
27 | ? webtorrentOptions.videoFiles | 38 | ? webtorrentOptions.videoFiles |
28 | // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode | 39 | // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode |
diff --git a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts index 18cb6750f..a7ee91950 100644 --- a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts +++ b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts | |||
@@ -2,13 +2,22 @@ import { basename } from 'path' | |||
2 | import { Segment } from '@peertube/p2p-media-loader-core' | 2 | import { Segment } from '@peertube/p2p-media-loader-core' |
3 | import { logger } from '@root-helpers/logger' | 3 | import { logger } from '@root-helpers/logger' |
4 | import { wait } from '@root-helpers/utils' | 4 | import { wait } from '@root-helpers/utils' |
5 | import { isSameOrigin } from '../common' | ||
5 | 6 | ||
6 | type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string } } | 7 | type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string } } |
7 | 8 | ||
8 | const maxRetries = 3 | 9 | const maxRetries = 3 |
9 | 10 | ||
10 | function segmentValidatorFactory (segmentsSha256Url: string, isLive: boolean) { | 11 | function segmentValidatorFactory (options: { |
11 | let segmentsJSON = fetchSha256Segments(segmentsSha256Url) | 12 | serverUrl: string |
13 | segmentsSha256Url: string | ||
14 | isLive: boolean | ||
15 | authorizationHeader: () => string | ||
16 | requiresAuth: boolean | ||
17 | }) { | ||
18 | const { serverUrl, segmentsSha256Url, isLive, authorizationHeader, requiresAuth } = options | ||
19 | |||
20 | let segmentsJSON = fetchSha256Segments({ serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth }) | ||
12 | const regex = /bytes=(\d+)-(\d+)/ | 21 | const regex = /bytes=(\d+)-(\d+)/ |
13 | 22 | ||
14 | return async function segmentValidator (segment: Segment, _method: string, _peerId: string, retry = 1) { | 23 | return async function segmentValidator (segment: Segment, _method: string, _peerId: string, retry = 1) { |
@@ -28,7 +37,7 @@ function segmentValidatorFactory (segmentsSha256Url: string, isLive: boolean) { | |||
28 | 37 | ||
29 | await wait(1000) | 38 | await wait(1000) |
30 | 39 | ||
31 | segmentsJSON = fetchSha256Segments(segmentsSha256Url) | 40 | segmentsJSON = fetchSha256Segments({ serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth }) |
32 | await segmentValidator(segment, _method, _peerId, retry + 1) | 41 | await segmentValidator(segment, _method, _peerId, retry + 1) |
33 | 42 | ||
34 | return | 43 | return |
@@ -68,8 +77,19 @@ export { | |||
68 | 77 | ||
69 | // --------------------------------------------------------------------------- | 78 | // --------------------------------------------------------------------------- |
70 | 79 | ||
71 | function fetchSha256Segments (url: string) { | 80 | function fetchSha256Segments (options: { |
72 | return fetch(url) | 81 | serverUrl: string |
82 | segmentsSha256Url: string | ||
83 | authorizationHeader: () => string | ||
84 | requiresAuth: boolean | ||
85 | }) { | ||
86 | const { serverUrl, segmentsSha256Url, requiresAuth, authorizationHeader } = options | ||
87 | |||
88 | const headers = requiresAuth && isSameOrigin(serverUrl, segmentsSha256Url) | ||
89 | ? { Authorization: authorizationHeader() } | ||
90 | : {} | ||
91 | |||
92 | return fetch(segmentsSha256Url, { headers }) | ||
73 | .then(res => res.json() as Promise<SegmentsJSON>) | 93 | .then(res => res.json() as Promise<SegmentsJSON>) |
74 | .catch(err => { | 94 | .catch(err => { |
75 | logger.error('Cannot get sha256 segments', err) | 95 | logger.error('Cannot get sha256 segments', err) |
diff --git a/client/src/assets/player/shared/peertube/peertube-plugin.ts b/client/src/assets/player/shared/peertube/peertube-plugin.ts index a5d712d70..4bd038bb1 100644 --- a/client/src/assets/player/shared/peertube/peertube-plugin.ts +++ b/client/src/assets/player/shared/peertube/peertube-plugin.ts | |||
@@ -22,7 +22,7 @@ const Plugin = videojs.getPlugin('plugin') | |||
22 | 22 | ||
23 | class PeerTubePlugin extends Plugin { | 23 | class PeerTubePlugin extends Plugin { |
24 | private readonly videoViewUrl: string | 24 | private readonly videoViewUrl: string |
25 | private readonly authorizationHeader: string | 25 | private readonly authorizationHeader: () => string |
26 | 26 | ||
27 | private readonly videoUUID: string | 27 | private readonly videoUUID: string |
28 | private readonly startTime: number | 28 | private readonly startTime: number |
@@ -228,7 +228,7 @@ class PeerTubePlugin extends Plugin { | |||
228 | 'Content-type': 'application/json; charset=UTF-8' | 228 | 'Content-type': 'application/json; charset=UTF-8' |
229 | }) | 229 | }) |
230 | 230 | ||
231 | if (this.authorizationHeader) headers.set('Authorization', this.authorizationHeader) | 231 | if (this.authorizationHeader) headers.set('Authorization', this.authorizationHeader()) |
232 | 232 | ||
233 | return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers }) | 233 | return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers }) |
234 | } | 234 | } |
diff --git a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts index fa3f48a9a..658b7c867 100644 --- a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts +++ b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts | |||
@@ -2,7 +2,7 @@ import videojs from 'video.js' | |||
2 | import * as WebTorrent from 'webtorrent' | 2 | import * as WebTorrent from 'webtorrent' |
3 | import { logger } from '@root-helpers/logger' | 3 | import { logger } from '@root-helpers/logger' |
4 | import { isIOS } from '@root-helpers/web-browser' | 4 | import { isIOS } from '@root-helpers/web-browser' |
5 | import { timeToInt } from '@shared/core-utils' | 5 | import { addQueryParams, timeToInt } from '@shared/core-utils' |
6 | import { VideoFile } from '@shared/models' | 6 | import { VideoFile } from '@shared/models' |
7 | import { getAverageBandwidthInStore, getStoredMute, getStoredVolume, saveAverageBandwidth } from '../../peertube-player-local-storage' | 7 | import { getAverageBandwidthInStore, getStoredMute, getStoredVolume, saveAverageBandwidth } from '../../peertube-player-local-storage' |
8 | import { PeerTubeResolution, PlayerNetworkInfo, WebtorrentPluginOptions } from '../../types' | 8 | import { PeerTubeResolution, PlayerNetworkInfo, WebtorrentPluginOptions } from '../../types' |
@@ -38,6 +38,8 @@ class WebTorrentPlugin extends Plugin { | |||
38 | BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth | 38 | BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth |
39 | } | 39 | } |
40 | 40 | ||
41 | private readonly buildWebSeedUrls: (file: VideoFile) => string[] | ||
42 | |||
41 | private readonly webtorrent = new WebTorrent({ | 43 | private readonly webtorrent = new WebTorrent({ |
42 | tracker: { | 44 | tracker: { |
43 | rtcConfig: getRtcConfig() | 45 | rtcConfig: getRtcConfig() |
@@ -57,6 +59,9 @@ class WebTorrentPlugin extends Plugin { | |||
57 | private isAutoResolutionObservation = false | 59 | private isAutoResolutionObservation = false |
58 | private playerRefusedP2P = false | 60 | private playerRefusedP2P = false |
59 | 61 | ||
62 | private requiresAuth: boolean | ||
63 | private videoFileToken: () => string | ||
64 | |||
60 | private torrentInfoInterval: any | 65 | private torrentInfoInterval: any |
61 | private autoQualityInterval: any | 66 | private autoQualityInterval: any |
62 | private addTorrentDelay: any | 67 | private addTorrentDelay: any |
@@ -81,6 +86,11 @@ class WebTorrentPlugin extends Plugin { | |||
81 | this.savePlayerSrcFunction = this.player.src | 86 | this.savePlayerSrcFunction = this.player.src |
82 | this.playerElement = options.playerElement | 87 | this.playerElement = options.playerElement |
83 | 88 | ||
89 | this.requiresAuth = options.requiresAuth | ||
90 | this.videoFileToken = options.videoFileToken | ||
91 | |||
92 | this.buildWebSeedUrls = options.buildWebSeedUrls | ||
93 | |||
84 | this.player.ready(() => { | 94 | this.player.ready(() => { |
85 | const playerOptions = this.player.options_ | 95 | const playerOptions = this.player.options_ |
86 | 96 | ||
@@ -268,7 +278,8 @@ class WebTorrentPlugin extends Plugin { | |||
268 | return new CacheChunkStore(new PeertubeChunkStore(chunkLength, storeOpts), { | 278 | return new CacheChunkStore(new PeertubeChunkStore(chunkLength, storeOpts), { |
269 | max: 100 | 279 | max: 100 |
270 | }) | 280 | }) |
271 | } | 281 | }, |
282 | urlList: this.buildWebSeedUrls(this.currentVideoFile) | ||
272 | } | 283 | } |
273 | 284 | ||
274 | this.torrent = this.webtorrent.add(magnetOrTorrentUrl, torrentOptions, torrent => { | 285 | this.torrent = this.webtorrent.add(magnetOrTorrentUrl, torrentOptions, torrent => { |
@@ -533,7 +544,12 @@ class WebTorrentPlugin extends Plugin { | |||
533 | // Enable error display now this is our last fallback | 544 | // Enable error display now this is our last fallback |
534 | this.player.one('error', () => this.player.peertube().displayFatalError()) | 545 | this.player.one('error', () => this.player.peertube().displayFatalError()) |
535 | 546 | ||
536 | const httpUrl = this.currentVideoFile.fileUrl | 547 | let httpUrl = this.currentVideoFile.fileUrl |
548 | |||
549 | if (this.requiresAuth && this.videoFileToken) { | ||
550 | httpUrl = addQueryParams(httpUrl, { videoFileToken: this.videoFileToken() }) | ||
551 | } | ||
552 | |||
537 | this.player.src = this.savePlayerSrcFunction | 553 | this.player.src = this.savePlayerSrcFunction |
538 | this.player.src(httpUrl) | 554 | this.player.src(httpUrl) |
539 | 555 | ||
diff --git a/client/src/assets/player/types/manager-options.ts b/client/src/assets/player/types/manager-options.ts index b4d9374c3..9da8fedf8 100644 --- a/client/src/assets/player/types/manager-options.ts +++ b/client/src/assets/player/types/manager-options.ts | |||
@@ -57,7 +57,7 @@ export interface CommonOptions extends CustomizationOptions { | |||
57 | captions: boolean | 57 | captions: boolean |
58 | 58 | ||
59 | videoViewUrl: string | 59 | videoViewUrl: string |
60 | authorizationHeader?: string | 60 | authorizationHeader?: () => string |
61 | 61 | ||
62 | metricsUrl: string | 62 | metricsUrl: string |
63 | 63 | ||
@@ -77,6 +77,8 @@ export interface CommonOptions extends CustomizationOptions { | |||
77 | videoShortUUID: string | 77 | videoShortUUID: string |
78 | 78 | ||
79 | serverUrl: string | 79 | serverUrl: string |
80 | requiresAuth: boolean | ||
81 | videoFileToken: () => string | ||
80 | 82 | ||
81 | errorNotifier: (message: string) => void | 83 | errorNotifier: (message: string) => void |
82 | } | 84 | } |
diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts index 6df94992c..037c4b74b 100644 --- a/client/src/assets/player/types/peertube-videojs-typings.ts +++ b/client/src/assets/player/types/peertube-videojs-typings.ts | |||
@@ -95,7 +95,7 @@ type PeerTubePluginOptions = { | |||
95 | videoDuration: number | 95 | videoDuration: number |
96 | 96 | ||
97 | videoViewUrl: string | 97 | videoViewUrl: string |
98 | authorizationHeader?: string | 98 | authorizationHeader?: () => string |
99 | 99 | ||
100 | subtitle?: string | 100 | subtitle?: string |
101 | 101 | ||
@@ -151,6 +151,11 @@ type WebtorrentPluginOptions = { | |||
151 | startTime: number | string | 151 | startTime: number | string |
152 | 152 | ||
153 | playerRefusedP2P: boolean | 153 | playerRefusedP2P: boolean |
154 | |||
155 | requiresAuth: boolean | ||
156 | videoFileToken: () => string | ||
157 | |||
158 | buildWebSeedUrls: (file: VideoFile) => string[] | ||
154 | } | 159 | } |
155 | 160 | ||
156 | type P2PMediaLoaderPluginOptions = { | 161 | type P2PMediaLoaderPluginOptions = { |
diff --git a/client/src/root-helpers/logger.ts b/client/src/root-helpers/logger.ts index 0d486c433..d1fdf73aa 100644 --- a/client/src/root-helpers/logger.ts +++ b/client/src/root-helpers/logger.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { ClientLogCreate } from '@shared/models/server' | 1 | import { ClientLogCreate } from '@shared/models/server' |
2 | import { peertubeLocalStorage } from './peertube-web-storage' | 2 | import { peertubeLocalStorage } from './peertube-web-storage' |
3 | import { UserTokens } from './users' | 3 | import { OAuthUserTokens } from './users' |
4 | 4 | ||
5 | export type LoggerHook = (message: LoggerMessage, meta?: LoggerMeta) => void | 5 | export type LoggerHook = (message: LoggerMessage, meta?: LoggerMeta) => void |
6 | export type LoggerLevel = 'info' | 'warn' | 'error' | 6 | export type LoggerLevel = 'info' | 'warn' | 'error' |
@@ -56,7 +56,7 @@ class Logger { | |||
56 | }) | 56 | }) |
57 | 57 | ||
58 | try { | 58 | try { |
59 | const tokens = UserTokens.getUserTokens(peertubeLocalStorage) | 59 | const tokens = OAuthUserTokens.getUserTokens(peertubeLocalStorage) |
60 | 60 | ||
61 | if (tokens) headers.set('Authorization', `${tokens.tokenType} ${tokens.accessToken}`) | 61 | if (tokens) headers.set('Authorization', `${tokens.tokenType} ${tokens.accessToken}`) |
62 | } catch (err) { | 62 | } catch (err) { |
diff --git a/client/src/root-helpers/users/index.ts b/client/src/root-helpers/users/index.ts index 2b11d0b7e..c03e67325 100644 --- a/client/src/root-helpers/users/index.ts +++ b/client/src/root-helpers/users/index.ts | |||
@@ -1,2 +1,2 @@ | |||
1 | export * from './user-local-storage-keys' | 1 | export * from './user-local-storage-keys' |
2 | export * from './user-tokens' | 2 | export * from './oauth-user-tokens' |
diff --git a/client/src/root-helpers/users/user-tokens.ts b/client/src/root-helpers/users/oauth-user-tokens.ts index a6d614cb7..a24e76b91 100644 --- a/client/src/root-helpers/users/user-tokens.ts +++ b/client/src/root-helpers/users/oauth-user-tokens.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import { UserTokenLocalStorageKeys } from './user-local-storage-keys' | 1 | import { UserTokenLocalStorageKeys } from './user-local-storage-keys' |
2 | 2 | ||
3 | export class UserTokens { | 3 | export class OAuthUserTokens { |
4 | accessToken: string | 4 | accessToken: string |
5 | refreshToken: string | 5 | refreshToken: string |
6 | tokenType: string | 6 | tokenType: string |
7 | 7 | ||
8 | constructor (hash?: Partial<UserTokens>) { | 8 | constructor (hash?: Partial<OAuthUserTokens>) { |
9 | if (hash) { | 9 | if (hash) { |
10 | this.accessToken = hash.accessToken | 10 | this.accessToken = hash.accessToken |
11 | this.refreshToken = hash.refreshToken | 11 | this.refreshToken = hash.refreshToken |
@@ -25,14 +25,14 @@ export class UserTokens { | |||
25 | 25 | ||
26 | if (!accessTokenLocalStorage || !refreshTokenLocalStorage || !tokenTypeLocalStorage) return null | 26 | if (!accessTokenLocalStorage || !refreshTokenLocalStorage || !tokenTypeLocalStorage) return null |
27 | 27 | ||
28 | return new UserTokens({ | 28 | return new OAuthUserTokens({ |
29 | accessToken: accessTokenLocalStorage, | 29 | accessToken: accessTokenLocalStorage, |
30 | refreshToken: refreshTokenLocalStorage, | 30 | refreshToken: refreshTokenLocalStorage, |
31 | tokenType: tokenTypeLocalStorage | 31 | tokenType: tokenTypeLocalStorage |
32 | }) | 32 | }) |
33 | } | 33 | } |
34 | 34 | ||
35 | static saveToLocalStorage (localStorage: Pick<Storage, 'setItem'>, tokens: UserTokens) { | 35 | static saveToLocalStorage (localStorage: Pick<Storage, 'setItem'>, tokens: OAuthUserTokens) { |
36 | localStorage.setItem(UserTokenLocalStorageKeys.ACCESS_TOKEN, tokens.accessToken) | 36 | localStorage.setItem(UserTokenLocalStorageKeys.ACCESS_TOKEN, tokens.accessToken) |
37 | localStorage.setItem(UserTokenLocalStorageKeys.REFRESH_TOKEN, tokens.refreshToken) | 37 | localStorage.setItem(UserTokenLocalStorageKeys.REFRESH_TOKEN, tokens.refreshToken) |
38 | localStorage.setItem(UserTokenLocalStorageKeys.TOKEN_TYPE, tokens.tokenType) | 38 | localStorage.setItem(UserTokenLocalStorageKeys.TOKEN_TYPE, tokens.tokenType) |
diff --git a/client/src/root-helpers/video.ts b/client/src/root-helpers/video.ts index ba84e49ea..107ba1eba 100644 --- a/client/src/root-helpers/video.ts +++ b/client/src/root-helpers/video.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { HTMLServerConfig, Video } from '@shared/models' | 1 | import { HTMLServerConfig, Video, VideoPrivacy } from '@shared/models' |
2 | 2 | ||
3 | function buildVideoOrPlaylistEmbed (options: { | 3 | function buildVideoOrPlaylistEmbed (options: { |
4 | embedUrl: string | 4 | embedUrl: string |
@@ -26,9 +26,14 @@ function isP2PEnabled (video: Video, config: HTMLServerConfig, userP2PEnabled: b | |||
26 | return userP2PEnabled | 26 | return userP2PEnabled |
27 | } | 27 | } |
28 | 28 | ||
29 | function videoRequiresAuth (video: Video) { | ||
30 | return new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]).has(video.privacy.id) | ||
31 | } | ||
32 | |||
29 | export { | 33 | export { |
30 | buildVideoOrPlaylistEmbed, | 34 | buildVideoOrPlaylistEmbed, |
31 | isP2PEnabled | 35 | isP2PEnabled, |
36 | videoRequiresAuth | ||
32 | } | 37 | } |
33 | 38 | ||
34 | // --------------------------------------------------------------------------- | 39 | // --------------------------------------------------------------------------- |
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index 451e54840..c6160151a 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts | |||
@@ -6,7 +6,7 @@ import { peertubeTranslate } from '../../../../shared/core-utils/i18n' | |||
6 | import { HTMLServerConfig, LiveVideo, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement } from '../../../../shared/models' | 6 | import { HTMLServerConfig, LiveVideo, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement } from '../../../../shared/models' |
7 | import { PeertubePlayerManager } from '../../assets/player' | 7 | import { PeertubePlayerManager } from '../../assets/player' |
8 | import { TranslationsManager } from '../../assets/player/translations-manager' | 8 | import { TranslationsManager } from '../../assets/player/translations-manager' |
9 | import { getParamString, logger } from '../../root-helpers' | 9 | import { getParamString, logger, videoRequiresAuth } from '../../root-helpers' |
10 | import { PeerTubeEmbedApi } from './embed-api' | 10 | import { PeerTubeEmbedApi } from './embed-api' |
11 | import { AuthHTTP, LiveManager, PeerTubePlugin, PlayerManagerOptions, PlaylistFetcher, PlaylistTracker, VideoFetcher } from './shared' | 11 | import { AuthHTTP, LiveManager, PeerTubePlugin, PlayerManagerOptions, PlaylistFetcher, PlaylistTracker, VideoFetcher } from './shared' |
12 | import { PlayerHTML } from './shared/player-html' | 12 | import { PlayerHTML } from './shared/player-html' |
@@ -167,22 +167,25 @@ export class PeerTubeEmbed { | |||
167 | private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) { | 167 | private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) { |
168 | const alreadyHadPlayer = this.resetPlayerElement() | 168 | const alreadyHadPlayer = this.resetPlayerElement() |
169 | 169 | ||
170 | const videoInfoPromise: Promise<{ video: VideoDetails, live?: LiveVideo }> = videoResponse.json() | 170 | const videoInfoPromise = videoResponse.json() |
171 | .then((videoInfo: VideoDetails) => { | 171 | .then(async (videoInfo: VideoDetails) => { |
172 | this.playerManagerOptions.loadParams(this.config, videoInfo) | 172 | this.playerManagerOptions.loadParams(this.config, videoInfo) |
173 | 173 | ||
174 | if (!alreadyHadPlayer && !this.playerManagerOptions.hasAutoplay()) { | 174 | if (!alreadyHadPlayer && !this.playerManagerOptions.hasAutoplay()) { |
175 | this.playerHTML.buildPlaceholder(videoInfo) | 175 | this.playerHTML.buildPlaceholder(videoInfo) |
176 | } | 176 | } |
177 | const live = videoInfo.isLive | ||
178 | ? await this.videoFetcher.loadLive(videoInfo) | ||
179 | : undefined | ||
177 | 180 | ||
178 | if (!videoInfo.isLive) { | 181 | const videoFileToken = videoRequiresAuth(videoInfo) |
179 | return { video: videoInfo } | 182 | ? await this.videoFetcher.loadVideoToken(videoInfo) |
180 | } | 183 | : undefined |
181 | 184 | ||
182 | return this.videoFetcher.loadVideoWithLive(videoInfo) | 185 | return { live, video: videoInfo, videoFileToken } |
183 | }) | 186 | }) |
184 | 187 | ||
185 | const [ { video, live }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([ | 188 | const [ { video, live, videoFileToken }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([ |
186 | videoInfoPromise, | 189 | videoInfoPromise, |
187 | this.translationsPromise, | 190 | this.translationsPromise, |
188 | captionsPromise, | 191 | captionsPromise, |
@@ -200,6 +203,9 @@ export class PeerTubeEmbed { | |||
200 | translations, | 203 | translations, |
201 | serverConfig: this.config, | 204 | serverConfig: this.config, |
202 | 205 | ||
206 | authorizationHeader: () => this.http.getHeaderTokenValue(), | ||
207 | videoFileToken: () => videoFileToken, | ||
208 | |||
203 | onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer(uuid), | 209 | onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer(uuid), |
204 | 210 | ||
205 | playlistTracker: this.playlistTracker, | 211 | playlistTracker: this.playlistTracker, |
diff --git a/client/src/standalone/videos/shared/auth-http.ts b/client/src/standalone/videos/shared/auth-http.ts index 0356ab8a6..43af5dff4 100644 --- a/client/src/standalone/videos/shared/auth-http.ts +++ b/client/src/standalone/videos/shared/auth-http.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { HttpStatusCode, OAuth2ErrorCode, UserRefreshToken } from '../../../../../shared/models' | 1 | import { HttpStatusCode, OAuth2ErrorCode, UserRefreshToken } from '../../../../../shared/models' |
2 | import { objectToUrlEncoded, UserTokens } from '../../../root-helpers' | 2 | import { OAuthUserTokens, objectToUrlEncoded } from '../../../root-helpers' |
3 | import { peertubeLocalStorage } from '../../../root-helpers/peertube-web-storage' | 3 | import { peertubeLocalStorage } from '../../../root-helpers/peertube-web-storage' |
4 | 4 | ||
5 | export class AuthHTTP { | 5 | export class AuthHTTP { |
@@ -8,30 +8,30 @@ export class AuthHTTP { | |||
8 | CLIENT_SECRET: 'client_secret' | 8 | CLIENT_SECRET: 'client_secret' |
9 | } | 9 | } |
10 | 10 | ||
11 | private userTokens: UserTokens | 11 | private userOAuthTokens: OAuthUserTokens |
12 | 12 | ||
13 | private headers = new Headers() | 13 | private headers = new Headers() |
14 | 14 | ||
15 | constructor () { | 15 | constructor () { |
16 | this.userTokens = UserTokens.getUserTokens(peertubeLocalStorage) | 16 | this.userOAuthTokens = OAuthUserTokens.getUserTokens(peertubeLocalStorage) |
17 | 17 | ||
18 | if (this.userTokens) this.setHeadersFromTokens() | 18 | if (this.userOAuthTokens) this.setHeadersFromTokens() |
19 | } | 19 | } |
20 | 20 | ||
21 | fetch (url: string, { optionalAuth }: { optionalAuth: boolean }) { | 21 | fetch (url: string, { optionalAuth, method }: { optionalAuth: boolean, method?: string }) { |
22 | const refreshFetchOptions = optionalAuth | 22 | const refreshFetchOptions = optionalAuth |
23 | ? { headers: this.headers } | 23 | ? { headers: this.headers } |
24 | : {} | 24 | : {} |
25 | 25 | ||
26 | return this.refreshFetch(url.toString(), refreshFetchOptions) | 26 | return this.refreshFetch(url.toString(), { ...refreshFetchOptions, method }) |
27 | } | 27 | } |
28 | 28 | ||
29 | getHeaderTokenValue () { | 29 | getHeaderTokenValue () { |
30 | return `${this.userTokens.tokenType} ${this.userTokens.accessToken}` | 30 | return `${this.userOAuthTokens.tokenType} ${this.userOAuthTokens.accessToken}` |
31 | } | 31 | } |
32 | 32 | ||
33 | isLoggedIn () { | 33 | isLoggedIn () { |
34 | return !!this.userTokens | 34 | return !!this.userOAuthTokens |
35 | } | 35 | } |
36 | 36 | ||
37 | private refreshFetch (url: string, options?: RequestInit) { | 37 | private refreshFetch (url: string, options?: RequestInit) { |
@@ -47,7 +47,7 @@ export class AuthHTTP { | |||
47 | headers.set('Content-Type', 'application/x-www-form-urlencoded') | 47 | headers.set('Content-Type', 'application/x-www-form-urlencoded') |
48 | 48 | ||
49 | const data = { | 49 | const data = { |
50 | refresh_token: this.userTokens.refreshToken, | 50 | refresh_token: this.userOAuthTokens.refreshToken, |
51 | client_id: clientId, | 51 | client_id: clientId, |
52 | client_secret: clientSecret, | 52 | client_secret: clientSecret, |
53 | response_type: 'code', | 53 | response_type: 'code', |
@@ -64,15 +64,15 @@ export class AuthHTTP { | |||
64 | return res.json() | 64 | return res.json() |
65 | }).then((obj: UserRefreshToken & { code?: OAuth2ErrorCode }) => { | 65 | }).then((obj: UserRefreshToken & { code?: OAuth2ErrorCode }) => { |
66 | if (!obj || obj.code === OAuth2ErrorCode.INVALID_GRANT) { | 66 | if (!obj || obj.code === OAuth2ErrorCode.INVALID_GRANT) { |
67 | UserTokens.flushLocalStorage(peertubeLocalStorage) | 67 | OAuthUserTokens.flushLocalStorage(peertubeLocalStorage) |
68 | this.removeTokensFromHeaders() | 68 | this.removeTokensFromHeaders() |
69 | 69 | ||
70 | return resolve() | 70 | return resolve() |
71 | } | 71 | } |
72 | 72 | ||
73 | this.userTokens.accessToken = obj.access_token | 73 | this.userOAuthTokens.accessToken = obj.access_token |
74 | this.userTokens.refreshToken = obj.refresh_token | 74 | this.userOAuthTokens.refreshToken = obj.refresh_token |
75 | UserTokens.saveToLocalStorage(peertubeLocalStorage, this.userTokens) | 75 | OAuthUserTokens.saveToLocalStorage(peertubeLocalStorage, this.userOAuthTokens) |
76 | 76 | ||
77 | this.setHeadersFromTokens() | 77 | this.setHeadersFromTokens() |
78 | 78 | ||
@@ -84,7 +84,7 @@ export class AuthHTTP { | |||
84 | 84 | ||
85 | return refreshingTokenPromise | 85 | return refreshingTokenPromise |
86 | .catch(() => { | 86 | .catch(() => { |
87 | UserTokens.flushLocalStorage(peertubeLocalStorage) | 87 | OAuthUserTokens.flushLocalStorage(peertubeLocalStorage) |
88 | 88 | ||
89 | this.removeTokensFromHeaders() | 89 | this.removeTokensFromHeaders() |
90 | }).then(() => fetch(url, { | 90 | }).then(() => fetch(url, { |
diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-manager-options.ts index eed821994..87a84975b 100644 --- a/client/src/standalone/videos/shared/player-manager-options.ts +++ b/client/src/standalone/videos/shared/player-manager-options.ts | |||
@@ -17,7 +17,8 @@ import { | |||
17 | isP2PEnabled, | 17 | isP2PEnabled, |
18 | logger, | 18 | logger, |
19 | peertubeLocalStorage, | 19 | peertubeLocalStorage, |
20 | UserLocalStorageKeys | 20 | UserLocalStorageKeys, |
21 | videoRequiresAuth | ||
21 | } from '../../../root-helpers' | 22 | } from '../../../root-helpers' |
22 | import { PeerTubePlugin } from './peertube-plugin' | 23 | import { PeerTubePlugin } from './peertube-plugin' |
23 | import { PlayerHTML } from './player-html' | 24 | import { PlayerHTML } from './player-html' |
@@ -154,6 +155,9 @@ export class PlayerManagerOptions { | |||
154 | captionsResponse: Response | 155 | captionsResponse: Response |
155 | live?: LiveVideo | 156 | live?: LiveVideo |
156 | 157 | ||
158 | authorizationHeader: () => string | ||
159 | videoFileToken: () => string | ||
160 | |||
157 | serverConfig: HTMLServerConfig | 161 | serverConfig: HTMLServerConfig |
158 | 162 | ||
159 | alreadyHadPlayer: boolean | 163 | alreadyHadPlayer: boolean |
@@ -169,9 +173,11 @@ export class PlayerManagerOptions { | |||
169 | video, | 173 | video, |
170 | captionsResponse, | 174 | captionsResponse, |
171 | alreadyHadPlayer, | 175 | alreadyHadPlayer, |
176 | videoFileToken, | ||
172 | translations, | 177 | translations, |
173 | playlistTracker, | 178 | playlistTracker, |
174 | live, | 179 | live, |
180 | authorizationHeader, | ||
175 | serverConfig | 181 | serverConfig |
176 | } = options | 182 | } = options |
177 | 183 | ||
@@ -227,6 +233,10 @@ export class PlayerManagerOptions { | |||
227 | embedUrl: window.location.origin + video.embedPath, | 233 | embedUrl: window.location.origin + video.embedPath, |
228 | embedTitle: video.name, | 234 | embedTitle: video.name, |
229 | 235 | ||
236 | requiresAuth: videoRequiresAuth(video), | ||
237 | authorizationHeader, | ||
238 | videoFileToken, | ||
239 | |||
230 | errorNotifier: () => { | 240 | errorNotifier: () => { |
231 | // Empty, we don't have a notifier in the embed | 241 | // Empty, we don't have a notifier in the embed |
232 | }, | 242 | }, |
diff --git a/client/src/standalone/videos/shared/video-fetcher.ts b/client/src/standalone/videos/shared/video-fetcher.ts index b42d622f9..cf6d12831 100644 --- a/client/src/standalone/videos/shared/video-fetcher.ts +++ b/client/src/standalone/videos/shared/video-fetcher.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { HttpStatusCode, LiveVideo, VideoDetails } from '../../../../../shared/models' | 1 | import { HttpStatusCode, LiveVideo, VideoDetails, VideoToken } from '../../../../../shared/models' |
2 | import { logger } from '../../../root-helpers' | 2 | import { logger } from '../../../root-helpers' |
3 | import { AuthHTTP } from './auth-http' | 3 | import { AuthHTTP } from './auth-http' |
4 | 4 | ||
@@ -36,10 +36,15 @@ export class VideoFetcher { | |||
36 | return { captionsPromise, videoResponse } | 36 | return { captionsPromise, videoResponse } |
37 | } | 37 | } |
38 | 38 | ||
39 | loadVideoWithLive (video: VideoDetails) { | 39 | loadLive (video: VideoDetails) { |
40 | return this.http.fetch(this.getLiveUrl(video.uuid), { optionalAuth: true }) | 40 | return this.http.fetch(this.getLiveUrl(video.uuid), { optionalAuth: true }) |
41 | .then(res => res.json()) | 41 | .then(res => res.json() as Promise<LiveVideo>) |
42 | .then((live: LiveVideo) => ({ video, live })) | 42 | } |
43 | |||
44 | loadVideoToken (video: VideoDetails) { | ||
45 | return this.http.fetch(this.getVideoTokenUrl(video.uuid), { optionalAuth: true, method: 'POST' }) | ||
46 | .then(res => res.json() as Promise<VideoToken>) | ||
47 | .then(token => token.files.token) | ||
43 | } | 48 | } |
44 | 49 | ||
45 | getVideoViewsUrl (videoUUID: string) { | 50 | getVideoViewsUrl (videoUUID: string) { |
@@ -61,4 +66,8 @@ export class VideoFetcher { | |||
61 | private getLiveUrl (videoId: string) { | 66 | private getLiveUrl (videoId: string) { |
62 | return window.location.origin + '/api/v1/videos/live/' + videoId | 67 | return window.location.origin + '/api/v1/videos/live/' + videoId |
63 | } | 68 | } |
69 | |||
70 | private getVideoTokenUrl (id: string) { | ||
71 | return this.getVideoUrl(id) + '/token' | ||
72 | } | ||
64 | } | 73 | } |
diff --git a/package.json b/package.json index 6dcf26253..23cd9e112 100644 --- a/package.json +++ b/package.json | |||
@@ -103,6 +103,7 @@ | |||
103 | "@peertube/http-signature": "^1.7.0", | 103 | "@peertube/http-signature": "^1.7.0", |
104 | "@uploadx/core": "^6.0.0", | 104 | "@uploadx/core": "^6.0.0", |
105 | "async-lru": "^1.1.1", | 105 | "async-lru": "^1.1.1", |
106 | "async-mutex": "^0.4.0", | ||
106 | "bcrypt": "5.0.1", | 107 | "bcrypt": "5.0.1", |
107 | "bencode": "^2.0.2", | 108 | "bencode": "^2.0.2", |
108 | "bittorrent-tracker": "^9.0.0", | 109 | "bittorrent-tracker": "^9.0.0", |
@@ -177,7 +178,6 @@ | |||
177 | }, | 178 | }, |
178 | "devDependencies": { | 179 | "devDependencies": { |
179 | "@peertube/maildev": "^1.2.0", | 180 | "@peertube/maildev": "^1.2.0", |
180 | "@types/async-lock": "^1.1.0", | ||
181 | "@types/bcrypt": "^5.0.0", | 181 | "@types/bcrypt": "^5.0.0", |
182 | "@types/bencode": "^2.0.0", | 182 | "@types/bencode": "^2.0.0", |
183 | "@types/bluebird": "^3.5.33", | 183 | "@types/bluebird": "^3.5.33", |
diff --git a/scripts/migrations/peertube-2.1.ts b/scripts/migrations/peertube-2.1.ts deleted file mode 100644 index 2e316d996..000000000 --- a/scripts/migrations/peertube-2.1.ts +++ /dev/null | |||
@@ -1,74 +0,0 @@ | |||
1 | import { pathExists, stat, writeFile } from 'fs-extra' | ||
2 | import parseTorrent from 'parse-torrent' | ||
3 | import { join } from 'path' | ||
4 | import * as Sequelize from 'sequelize' | ||
5 | import { logger } from '@server/helpers/logger' | ||
6 | import { createTorrentPromise } from '@server/helpers/webtorrent' | ||
7 | import { CONFIG } from '@server/initializers/config' | ||
8 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, STATIC_PATHS, WEBSERVER } from '@server/initializers/constants' | ||
9 | import { initDatabaseModels, sequelizeTypescript } from '../../server/initializers/database' | ||
10 | |||
11 | run() | ||
12 | .then(() => process.exit(0)) | ||
13 | .catch(err => { | ||
14 | console.error(err) | ||
15 | process.exit(-1) | ||
16 | }) | ||
17 | |||
18 | async function run () { | ||
19 | logger.info('Creating torrents and updating database for HSL files.') | ||
20 | |||
21 | await initDatabaseModels(true) | ||
22 | |||
23 | const query = 'select "videoFile".id as id, "videoFile".resolution as resolution, "video".uuid as uuid from "videoFile" ' + | ||
24 | 'inner join "videoStreamingPlaylist" ON "videoStreamingPlaylist".id = "videoFile"."videoStreamingPlaylistId" ' + | ||
25 | 'inner join video ON video.id = "videoStreamingPlaylist"."videoId" ' + | ||
26 | 'WHERE video.remote IS FALSE' | ||
27 | const options = { | ||
28 | type: Sequelize.QueryTypes.SELECT | ||
29 | } | ||
30 | const res = await sequelizeTypescript.query(query, options) | ||
31 | |||
32 | for (const row of res) { | ||
33 | const videoFilename = `${row['uuid']}-${row['resolution']}-fragmented.mp4` | ||
34 | const videoFilePath = join(HLS_STREAMING_PLAYLIST_DIRECTORY, row['uuid'], videoFilename) | ||
35 | |||
36 | logger.info('Processing %s.', videoFilePath) | ||
37 | |||
38 | if (!await pathExists(videoFilePath)) { | ||
39 | console.warn('Cannot generate torrent of %s: file does not exist.', videoFilePath) | ||
40 | continue | ||
41 | } | ||
42 | |||
43 | const createTorrentOptions = { | ||
44 | // Keep the extname, it's used by the client to stream the file inside a web browser | ||
45 | name: `video ${row['uuid']}`, | ||
46 | createdBy: 'PeerTube', | ||
47 | announceList: [ | ||
48 | [ WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket' ], | ||
49 | [ WEBSERVER.URL + '/tracker/announce' ] | ||
50 | ], | ||
51 | urlList: [ WEBSERVER.URL + join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, row['uuid'], videoFilename) ] | ||
52 | } | ||
53 | const torrent = await createTorrentPromise(videoFilePath, createTorrentOptions) | ||
54 | |||
55 | const torrentName = `${row['uuid']}-${row['resolution']}-hls.torrent` | ||
56 | const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, torrentName) | ||
57 | |||
58 | await writeFile(filePath, torrent) | ||
59 | |||
60 | const parsedTorrent = parseTorrent(torrent) | ||
61 | const infoHash = parsedTorrent.infoHash | ||
62 | |||
63 | const stats = await stat(videoFilePath) | ||
64 | const size = stats.size | ||
65 | |||
66 | const queryUpdate = 'UPDATE "videoFile" SET "infoHash" = ?, "size" = ? WHERE id = ?' | ||
67 | |||
68 | const options = { | ||
69 | type: Sequelize.QueryTypes.UPDATE, | ||
70 | replacements: [ infoHash, size, row['id'] ] | ||
71 | } | ||
72 | await sequelizeTypescript.query(queryUpdate, options) | ||
73 | } | ||
74 | } | ||
diff --git a/scripts/prune-storage.ts b/scripts/prune-storage.ts index 3012bdb94..d19594a60 100755 --- a/scripts/prune-storage.ts +++ b/scripts/prune-storage.ts | |||
@@ -2,7 +2,7 @@ import { map } from 'bluebird' | |||
2 | import { readdir, remove, stat } from 'fs-extra' | 2 | import { readdir, remove, stat } from 'fs-extra' |
3 | import { basename, join } from 'path' | 3 | import { basename, join } from 'path' |
4 | import { get, start } from 'prompt' | 4 | import { get, start } from 'prompt' |
5 | import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY } from '@server/initializers/constants' | 5 | import { DIRECTORIES } from '@server/initializers/constants' |
6 | import { VideoFileModel } from '@server/models/video/video-file' | 6 | import { VideoFileModel } from '@server/models/video/video-file' |
7 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 7 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
8 | import { uniqify } from '@shared/core-utils' | 8 | import { uniqify } from '@shared/core-utils' |
@@ -37,9 +37,11 @@ async function run () { | |||
37 | console.log('Detecting files to remove, it could take a while...') | 37 | console.log('Detecting files to remove, it could take a while...') |
38 | 38 | ||
39 | toDelete = toDelete.concat( | 39 | toDelete = toDelete.concat( |
40 | await pruneDirectory(CONFIG.STORAGE.VIDEOS_DIR, doesWebTorrentFileExist()), | 40 | await pruneDirectory(DIRECTORIES.VIDEOS.PUBLIC, doesWebTorrentFileExist()), |
41 | await pruneDirectory(DIRECTORIES.VIDEOS.PRIVATE, doesWebTorrentFileExist()), | ||
41 | 42 | ||
42 | await pruneDirectory(HLS_STREAMING_PLAYLIST_DIRECTORY, doesHLSPlaylistExist()), | 43 | await pruneDirectory(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, doesHLSPlaylistExist()), |
44 | await pruneDirectory(DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC, doesHLSPlaylistExist()), | ||
43 | 45 | ||
44 | await pruneDirectory(CONFIG.STORAGE.TORRENTS_DIR, doesTorrentFileExist()), | 46 | await pruneDirectory(CONFIG.STORAGE.TORRENTS_DIR, doesTorrentFileExist()), |
45 | 47 | ||
@@ -75,7 +77,7 @@ async function run () { | |||
75 | } | 77 | } |
76 | } | 78 | } |
77 | 79 | ||
78 | type ExistFun = (file: string) => Promise<boolean> | 80 | type ExistFun = (file: string) => Promise<boolean> | boolean |
79 | async function pruneDirectory (directory: string, existFun: ExistFun) { | 81 | async function pruneDirectory (directory: string, existFun: ExistFun) { |
80 | const files = await readdir(directory) | 82 | const files = await readdir(directory) |
81 | 83 | ||
@@ -92,11 +94,21 @@ async function pruneDirectory (directory: string, existFun: ExistFun) { | |||
92 | } | 94 | } |
93 | 95 | ||
94 | function doesWebTorrentFileExist () { | 96 | function doesWebTorrentFileExist () { |
95 | return (filePath: string) => VideoFileModel.doesOwnedWebTorrentVideoFileExist(basename(filePath)) | 97 | return (filePath: string) => { |
98 | // Don't delete private directory | ||
99 | if (filePath === DIRECTORIES.VIDEOS.PRIVATE) return true | ||
100 | |||
101 | return VideoFileModel.doesOwnedWebTorrentVideoFileExist(basename(filePath)) | ||
102 | } | ||
96 | } | 103 | } |
97 | 104 | ||
98 | function doesHLSPlaylistExist () { | 105 | function doesHLSPlaylistExist () { |
99 | return (hlsPath: string) => VideoStreamingPlaylistModel.doesOwnedHLSPlaylistExist(basename(hlsPath)) | 106 | return (hlsPath: string) => { |
107 | // Don't delete private directory | ||
108 | if (hlsPath === DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE) return true | ||
109 | |||
110 | return VideoStreamingPlaylistModel.doesOwnedHLSPlaylistExist(basename(hlsPath)) | ||
111 | } | ||
100 | } | 112 | } |
101 | 113 | ||
102 | function doesTorrentFileExist () { | 114 | function doesTorrentFileExist () { |
@@ -127,8 +139,8 @@ async function doesRedundancyExist (filePath: string) { | |||
127 | const isPlaylist = (await stat(filePath)).isDirectory() | 139 | const isPlaylist = (await stat(filePath)).isDirectory() |
128 | 140 | ||
129 | if (isPlaylist) { | 141 | if (isPlaylist) { |
130 | // Don't delete HLS directory | 142 | // Don't delete HLS redundancy directory |
131 | if (filePath === HLS_REDUNDANCY_DIRECTORY) return true | 143 | if (filePath === DIRECTORIES.HLS_REDUNDANCY) return true |
132 | 144 | ||
133 | const uuid = getUUIDFromFilename(filePath) | 145 | const uuid = getUUIDFromFilename(filePath) |
134 | const video = await VideoModel.loadWithFiles(uuid) | 146 | const video = await VideoModel.loadWithFiles(uuid) |
diff --git a/server/controllers/api/server/debug.ts b/server/controllers/api/server/debug.ts index 4e5333782..f3792bfc8 100644 --- a/server/controllers/api/server/debug.ts +++ b/server/controllers/api/server/debug.ts | |||
@@ -8,6 +8,7 @@ import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | |||
8 | import { UserRight } from '../../../../shared/models/users' | 8 | import { UserRight } from '../../../../shared/models/users' |
9 | import { authenticate, ensureUserHasRight } from '../../../middlewares' | 9 | import { authenticate, ensureUserHasRight } from '../../../middlewares' |
10 | import { VideoChannelSyncLatestScheduler } from '@server/lib/schedulers/video-channel-sync-latest-scheduler' | 10 | import { VideoChannelSyncLatestScheduler } from '@server/lib/schedulers/video-channel-sync-latest-scheduler' |
11 | import { UpdateVideosScheduler } from '@server/lib/schedulers/update-videos-scheduler' | ||
11 | 12 | ||
12 | const debugRouter = express.Router() | 13 | const debugRouter = express.Router() |
13 | 14 | ||
@@ -45,6 +46,7 @@ async function runCommand (req: express.Request, res: express.Response) { | |||
45 | 'remove-dandling-resumable-uploads': () => RemoveDanglingResumableUploadsScheduler.Instance.execute(), | 46 | 'remove-dandling-resumable-uploads': () => RemoveDanglingResumableUploadsScheduler.Instance.execute(), |
46 | 'process-video-views-buffer': () => VideoViewsBufferScheduler.Instance.execute(), | 47 | 'process-video-views-buffer': () => VideoViewsBufferScheduler.Instance.execute(), |
47 | 'process-video-viewers': () => VideoViewsManager.Instance.processViewerStats(), | 48 | 'process-video-viewers': () => VideoViewsManager.Instance.processViewerStats(), |
49 | 'process-update-videos-scheduler': () => UpdateVideosScheduler.Instance.execute(), | ||
48 | 'process-video-channel-sync-latest': () => VideoChannelSyncLatestScheduler.Instance.execute() | 50 | 'process-video-channel-sync-latest': () => VideoChannelSyncLatestScheduler.Instance.execute() |
49 | } | 51 | } |
50 | 52 | ||
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index b301515df..ea081e5ab 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -41,6 +41,7 @@ import { ownershipVideoRouter } from './ownership' | |||
41 | import { rateVideoRouter } from './rate' | 41 | import { rateVideoRouter } from './rate' |
42 | import { statsRouter } from './stats' | 42 | import { statsRouter } from './stats' |
43 | import { studioRouter } from './studio' | 43 | import { studioRouter } from './studio' |
44 | import { tokenRouter } from './token' | ||
44 | import { transcodingRouter } from './transcoding' | 45 | import { transcodingRouter } from './transcoding' |
45 | import { updateRouter } from './update' | 46 | import { updateRouter } from './update' |
46 | import { uploadRouter } from './upload' | 47 | import { uploadRouter } from './upload' |
@@ -63,6 +64,7 @@ videosRouter.use('/', uploadRouter) | |||
63 | videosRouter.use('/', updateRouter) | 64 | videosRouter.use('/', updateRouter) |
64 | videosRouter.use('/', filesRouter) | 65 | videosRouter.use('/', filesRouter) |
65 | videosRouter.use('/', transcodingRouter) | 66 | videosRouter.use('/', transcodingRouter) |
67 | videosRouter.use('/', tokenRouter) | ||
66 | 68 | ||
67 | videosRouter.get('/categories', | 69 | videosRouter.get('/categories', |
68 | openapiOperationDoc({ operationId: 'getCategories' }), | 70 | openapiOperationDoc({ operationId: 'getCategories' }), |
diff --git a/server/controllers/api/videos/token.ts b/server/controllers/api/videos/token.ts new file mode 100644 index 000000000..009b6dfb6 --- /dev/null +++ b/server/controllers/api/videos/token.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | import express from 'express' | ||
2 | import { VideoTokensManager } from '@server/lib/video-tokens-manager' | ||
3 | import { VideoToken } from '@shared/models' | ||
4 | import { asyncMiddleware, authenticate, videosCustomGetValidator } from '../../../middlewares' | ||
5 | |||
6 | const tokenRouter = express.Router() | ||
7 | |||
8 | tokenRouter.post('/:id/token', | ||
9 | authenticate, | ||
10 | asyncMiddleware(videosCustomGetValidator('only-video')), | ||
11 | generateToken | ||
12 | ) | ||
13 | |||
14 | // --------------------------------------------------------------------------- | ||
15 | |||
16 | export { | ||
17 | tokenRouter | ||
18 | } | ||
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | function generateToken (req: express.Request, res: express.Response) { | ||
23 | const video = res.locals.onlyVideo | ||
24 | |||
25 | const { token, expires } = VideoTokensManager.Instance.create(video.uuid) | ||
26 | |||
27 | return res.json({ | ||
28 | files: { | ||
29 | token, | ||
30 | expires | ||
31 | } | ||
32 | } as VideoToken) | ||
33 | } | ||
diff --git a/server/controllers/api/videos/update.ts b/server/controllers/api/videos/update.ts index ab1a23d9a..0a910379a 100644 --- a/server/controllers/api/videos/update.ts +++ b/server/controllers/api/videos/update.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | import express from 'express' | 1 | 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 { CreateJobArgument, JobQueue } from '@server/lib/job-queue' | 4 | import { addVideoJobsAfterUpdate, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' |
5 | import { buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | 5 | import { setVideoPrivacy } from '@server/lib/video-privacy' |
6 | import { openapiOperationDoc } from '@server/middlewares/doc' | 6 | import { openapiOperationDoc } from '@server/middlewares/doc' |
7 | import { FilteredModelAttributes } from '@server/types' | 7 | import { FilteredModelAttributes } from '@server/types' |
8 | import { MVideoFullLight } from '@server/types/models' | 8 | import { MVideoFullLight } from '@server/types/models' |
9 | import { HttpStatusCode, ManageVideoTorrentPayload, VideoUpdate } from '@shared/models' | 9 | import { HttpStatusCode, VideoUpdate } from '@shared/models' |
10 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 10 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
11 | import { resetSequelizeInstance } from '../../../helpers/database-utils' | 11 | import { resetSequelizeInstance } from '../../../helpers/database-utils' |
12 | import { createReqFiles } from '../../../helpers/express-utils' | 12 | import { createReqFiles } from '../../../helpers/express-utils' |
@@ -18,6 +18,7 @@ import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | |||
18 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares' | 18 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares' |
19 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | 19 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' |
20 | import { VideoModel } from '../../../models/video/video' | 20 | import { VideoModel } from '../../../models/video/video' |
21 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
21 | 22 | ||
22 | const lTags = loggerTagsFactory('api', 'video') | 23 | const lTags = loggerTagsFactory('api', 'video') |
23 | const auditLogger = auditLoggerFactory('videos') | 24 | const auditLogger = auditLoggerFactory('videos') |
@@ -47,8 +48,8 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
47 | const oldVideoAuditView = new VideoAuditView(videoFromReq.toFormattedDetailsJSON()) | 48 | const oldVideoAuditView = new VideoAuditView(videoFromReq.toFormattedDetailsJSON()) |
48 | const videoInfoToUpdate: VideoUpdate = req.body | 49 | const videoInfoToUpdate: VideoUpdate = req.body |
49 | 50 | ||
50 | const wasConfidentialVideo = videoFromReq.isConfidential() | ||
51 | const hadPrivacyForFederation = videoFromReq.hasPrivacyForFederation() | 51 | const hadPrivacyForFederation = videoFromReq.hasPrivacyForFederation() |
52 | const oldPrivacy = videoFromReq.privacy | ||
52 | 53 | ||
53 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | 54 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ |
54 | video: videoFromReq, | 55 | video: videoFromReq, |
@@ -57,12 +58,13 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
57 | automaticallyGenerated: false | 58 | automaticallyGenerated: false |
58 | }) | 59 | }) |
59 | 60 | ||
61 | const videoFileLockReleaser = await VideoPathManager.Instance.lockFiles(videoFromReq.uuid) | ||
62 | |||
60 | try { | 63 | try { |
61 | const { videoInstanceUpdated, isNewVideo } = await sequelizeTypescript.transaction(async t => { | 64 | const { videoInstanceUpdated, isNewVideo } = await sequelizeTypescript.transaction(async t => { |
62 | // Refresh video since thumbnails to prevent concurrent updates | 65 | // Refresh video since thumbnails to prevent concurrent updates |
63 | const video = await VideoModel.loadFull(videoFromReq.id, t) | 66 | const video = await VideoModel.loadFull(videoFromReq.id, t) |
64 | 67 | ||
65 | const sequelizeOptions = { transaction: t } | ||
66 | const oldVideoChannel = video.VideoChannel | 68 | const oldVideoChannel = video.VideoChannel |
67 | 69 | ||
68 | const keysToUpdate: (keyof VideoUpdate & FilteredModelAttributes<VideoModel>)[] = [ | 70 | const keysToUpdate: (keyof VideoUpdate & FilteredModelAttributes<VideoModel>)[] = [ |
@@ -97,7 +99,7 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
97 | await video.setAsRefreshed(t) | 99 | await video.setAsRefreshed(t) |
98 | } | 100 | } |
99 | 101 | ||
100 | const videoInstanceUpdated = await video.save(sequelizeOptions) as MVideoFullLight | 102 | const videoInstanceUpdated = await video.save({ transaction: t }) as MVideoFullLight |
101 | 103 | ||
102 | // Thumbnail & preview updates? | 104 | // Thumbnail & preview updates? |
103 | if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t) | 105 | if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t) |
@@ -113,7 +115,9 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
113 | await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t }) | 115 | await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t }) |
114 | videoInstanceUpdated.VideoChannel = res.locals.videoChannel | 116 | videoInstanceUpdated.VideoChannel = res.locals.videoChannel |
115 | 117 | ||
116 | if (hadPrivacyForFederation === true) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t) | 118 | if (hadPrivacyForFederation === true) { |
119 | await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t) | ||
120 | } | ||
117 | } | 121 | } |
118 | 122 | ||
119 | // Schedule an update in the future? | 123 | // Schedule an update in the future? |
@@ -139,7 +143,12 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
139 | 143 | ||
140 | Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body, req, res }) | 144 | Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body, req, res }) |
141 | 145 | ||
142 | await addVideoJobsAfterUpdate({ video: videoInstanceUpdated, videoInfoToUpdate, wasConfidentialVideo, isNewVideo }) | 146 | await addVideoJobsAfterUpdate({ |
147 | video: videoInstanceUpdated, | ||
148 | nameChanged: !!videoInfoToUpdate.name, | ||
149 | oldPrivacy, | ||
150 | isNewVideo | ||
151 | }) | ||
143 | } catch (err) { | 152 | } catch (err) { |
144 | // Force fields we want to update | 153 | // Force fields we want to update |
145 | // If the transaction is retried, sequelize will think the object has not changed | 154 | // If the transaction is retried, sequelize will think the object has not changed |
@@ -147,6 +156,8 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
147 | resetSequelizeInstance(videoFromReq, videoFieldsSave) | 156 | resetSequelizeInstance(videoFromReq, videoFieldsSave) |
148 | 157 | ||
149 | throw err | 158 | throw err |
159 | } finally { | ||
160 | videoFileLockReleaser() | ||
150 | } | 161 | } |
151 | 162 | ||
152 | return res.type('json') | 163 | return res.type('json') |
@@ -164,7 +175,7 @@ async function updateVideoPrivacy (options: { | |||
164 | const isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy) | 175 | const isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy) |
165 | 176 | ||
166 | const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10) | 177 | const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10) |
167 | videoInstance.setPrivacy(newPrivacy) | 178 | setVideoPrivacy(videoInstance, newPrivacy) |
168 | 179 | ||
169 | // Unfederate the video if the new privacy is not compatible with federation | 180 | // Unfederate the video if the new privacy is not compatible with federation |
170 | if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) { | 181 | if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) { |
@@ -185,50 +196,3 @@ function updateSchedule (videoInstance: MVideoFullLight, videoInfoToUpdate: Vide | |||
185 | return ScheduleVideoUpdateModel.deleteByVideoId(videoInstance.id, transaction) | 196 | return ScheduleVideoUpdateModel.deleteByVideoId(videoInstance.id, transaction) |
186 | } | 197 | } |
187 | } | 198 | } |
188 | |||
189 | async function addVideoJobsAfterUpdate (options: { | ||
190 | video: MVideoFullLight | ||
191 | videoInfoToUpdate: VideoUpdate | ||
192 | wasConfidentialVideo: boolean | ||
193 | isNewVideo: boolean | ||
194 | }) { | ||
195 | const { video, videoInfoToUpdate, wasConfidentialVideo, isNewVideo } = options | ||
196 | const jobs: CreateJobArgument[] = [] | ||
197 | |||
198 | if (!video.isLive && videoInfoToUpdate.name) { | ||
199 | |||
200 | for (const file of (video.VideoFiles || [])) { | ||
201 | const payload: ManageVideoTorrentPayload = { action: 'update-metadata', videoId: video.id, videoFileId: file.id } | ||
202 | |||
203 | jobs.push({ type: 'manage-video-torrent', payload }) | ||
204 | } | ||
205 | |||
206 | const hls = video.getHLSPlaylist() | ||
207 | |||
208 | for (const file of (hls?.VideoFiles || [])) { | ||
209 | const payload: ManageVideoTorrentPayload = { action: 'update-metadata', streamingPlaylistId: hls.id, videoFileId: file.id } | ||
210 | |||
211 | jobs.push({ type: 'manage-video-torrent', payload }) | ||
212 | } | ||
213 | } | ||
214 | |||
215 | jobs.push({ | ||
216 | type: 'federate-video', | ||
217 | payload: { | ||
218 | videoUUID: video.uuid, | ||
219 | isNewVideo | ||
220 | } | ||
221 | }) | ||
222 | |||
223 | if (wasConfidentialVideo) { | ||
224 | jobs.push({ | ||
225 | type: 'notify', | ||
226 | payload: { | ||
227 | action: 'new-video', | ||
228 | videoUUID: video.uuid | ||
229 | } | ||
230 | }) | ||
231 | } | ||
232 | |||
233 | return JobQueue.Instance.createSequentialJobFlow(...jobs) | ||
234 | } | ||
diff --git a/server/controllers/download.ts b/server/controllers/download.ts index a270180c0..abd1df26f 100644 --- a/server/controllers/download.ts +++ b/server/controllers/download.ts | |||
@@ -7,7 +7,7 @@ import { VideoPathManager } from '@server/lib/video-path-manager' | |||
7 | import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 7 | import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
8 | import { HttpStatusCode, VideoStorage, VideoStreamingPlaylistType } from '@shared/models' | 8 | import { HttpStatusCode, VideoStorage, VideoStreamingPlaylistType } from '@shared/models' |
9 | import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' | 9 | import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' |
10 | import { asyncMiddleware, videosDownloadValidator } from '../middlewares' | 10 | import { asyncMiddleware, optionalAuthenticate, videosDownloadValidator } from '../middlewares' |
11 | 11 | ||
12 | const downloadRouter = express.Router() | 12 | const downloadRouter = express.Router() |
13 | 13 | ||
@@ -20,12 +20,14 @@ downloadRouter.use( | |||
20 | 20 | ||
21 | downloadRouter.use( | 21 | downloadRouter.use( |
22 | STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', | 22 | STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', |
23 | optionalAuthenticate, | ||
23 | asyncMiddleware(videosDownloadValidator), | 24 | asyncMiddleware(videosDownloadValidator), |
24 | asyncMiddleware(downloadVideoFile) | 25 | asyncMiddleware(downloadVideoFile) |
25 | ) | 26 | ) |
26 | 27 | ||
27 | downloadRouter.use( | 28 | downloadRouter.use( |
28 | STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension', | 29 | STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension', |
30 | optionalAuthenticate, | ||
29 | asyncMiddleware(videosDownloadValidator), | 31 | asyncMiddleware(videosDownloadValidator), |
30 | asyncMiddleware(downloadHLSVideoFile) | 32 | asyncMiddleware(downloadHLSVideoFile) |
31 | ) | 33 | ) |
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 33c429eb1..dc091455a 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -1,20 +1,34 @@ | |||
1 | import cors from 'cors' | 1 | import cors from 'cors' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | import { handleStaticError } from '@server/middlewares' | 3 | import { |
4 | asyncMiddleware, | ||
5 | ensureCanAccessPrivateVideoHLSFiles, | ||
6 | ensureCanAccessVideoPrivateWebTorrentFiles, | ||
7 | handleStaticError, | ||
8 | optionalAuthenticate | ||
9 | } from '@server/middlewares' | ||
4 | import { CONFIG } from '../initializers/config' | 10 | import { CONFIG } from '../initializers/config' |
5 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers/constants' | 11 | import { DIRECTORIES, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers/constants' |
6 | 12 | ||
7 | const staticRouter = express.Router() | 13 | const staticRouter = express.Router() |
8 | 14 | ||
9 | // Cors is very important to let other servers access torrent and video files | 15 | // Cors is very important to let other servers access torrent and video files |
10 | staticRouter.use(cors()) | 16 | staticRouter.use(cors()) |
11 | 17 | ||
12 | // Videos path for webseed | 18 | // WebTorrent/Classic videos |
19 | staticRouter.use( | ||
20 | STATIC_PATHS.PRIVATE_WEBSEED, | ||
21 | optionalAuthenticate, | ||
22 | asyncMiddleware(ensureCanAccessVideoPrivateWebTorrentFiles), | ||
23 | express.static(DIRECTORIES.VIDEOS.PRIVATE, { fallthrough: false }), | ||
24 | handleStaticError | ||
25 | ) | ||
13 | staticRouter.use( | 26 | staticRouter.use( |
14 | STATIC_PATHS.WEBSEED, | 27 | STATIC_PATHS.WEBSEED, |
15 | express.static(CONFIG.STORAGE.VIDEOS_DIR, { fallthrough: false }), | 28 | express.static(DIRECTORIES.VIDEOS.PUBLIC, { fallthrough: false }), |
16 | handleStaticError | 29 | handleStaticError |
17 | ) | 30 | ) |
31 | |||
18 | staticRouter.use( | 32 | staticRouter.use( |
19 | STATIC_PATHS.REDUNDANCY, | 33 | STATIC_PATHS.REDUNDANCY, |
20 | express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }), | 34 | express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }), |
@@ -23,8 +37,15 @@ staticRouter.use( | |||
23 | 37 | ||
24 | // HLS | 38 | // HLS |
25 | staticRouter.use( | 39 | staticRouter.use( |
40 | STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, | ||
41 | optionalAuthenticate, | ||
42 | asyncMiddleware(ensureCanAccessPrivateVideoHLSFiles), | ||
43 | express.static(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, { fallthrough: false }), | ||
44 | handleStaticError | ||
45 | ) | ||
46 | staticRouter.use( | ||
26 | STATIC_PATHS.STREAMING_PLAYLISTS.HLS, | 47 | STATIC_PATHS.STREAMING_PLAYLISTS.HLS, |
27 | express.static(HLS_STREAMING_PLAYLIST_DIRECTORY, { fallthrough: false }), | 48 | express.static(DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC, { fallthrough: false }), |
28 | handleStaticError | 49 | handleStaticError |
29 | ) | 50 | ) |
30 | 51 | ||
diff --git a/server/helpers/ffmpeg/ffmpeg-vod.ts b/server/helpers/ffmpeg/ffmpeg-vod.ts index 7a81a1313..d84703eb9 100644 --- a/server/helpers/ffmpeg/ffmpeg-vod.ts +++ b/server/helpers/ffmpeg/ffmpeg-vod.ts | |||
@@ -1,14 +1,15 @@ | |||
1 | import { MutexInterface } from 'async-mutex' | ||
1 | import { Job } from 'bullmq' | 2 | import { Job } from 'bullmq' |
2 | import { FfmpegCommand } from 'fluent-ffmpeg' | 3 | import { FfmpegCommand } from 'fluent-ffmpeg' |
3 | import { readFile, writeFile } from 'fs-extra' | 4 | import { readFile, writeFile } from 'fs-extra' |
4 | import { dirname } from 'path' | 5 | import { dirname } from 'path' |
6 | import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants' | ||
5 | import { pick } from '@shared/core-utils' | 7 | import { pick } from '@shared/core-utils' |
6 | import { AvailableEncoders, VideoResolution } from '@shared/models' | 8 | import { AvailableEncoders, VideoResolution } from '@shared/models' |
7 | import { logger, loggerTagsFactory } from '../logger' | 9 | import { logger, loggerTagsFactory } from '../logger' |
8 | import { getFFmpeg, runCommand } from './ffmpeg-commons' | 10 | import { getFFmpeg, runCommand } from './ffmpeg-commons' |
9 | import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets' | 11 | import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets' |
10 | import { computeFPS, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from './ffprobe-utils' | 12 | import { computeFPS, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from './ffprobe-utils' |
11 | import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants' | ||
12 | 13 | ||
13 | const lTags = loggerTagsFactory('ffmpeg') | 14 | const lTags = loggerTagsFactory('ffmpeg') |
14 | 15 | ||
@@ -22,6 +23,10 @@ interface BaseTranscodeVODOptions { | |||
22 | inputPath: string | 23 | inputPath: string |
23 | outputPath: string | 24 | outputPath: string |
24 | 25 | ||
26 | // Will be released after the ffmpeg started | ||
27 | // To prevent a bug where the input file does not exist anymore when running ffmpeg | ||
28 | inputFileMutexReleaser: MutexInterface.Releaser | ||
29 | |||
25 | availableEncoders: AvailableEncoders | 30 | availableEncoders: AvailableEncoders |
26 | profile: string | 31 | profile: string |
27 | 32 | ||
@@ -94,6 +99,12 @@ async function transcodeVOD (options: TranscodeVODOptions) { | |||
94 | 99 | ||
95 | command = await builders[options.type](command, options) | 100 | command = await builders[options.type](command, options) |
96 | 101 | ||
102 | command.on('start', () => { | ||
103 | setTimeout(() => { | ||
104 | options.inputFileMutexReleaser() | ||
105 | }, 1000) | ||
106 | }) | ||
107 | |||
97 | await runCommand({ command, job: options.job }) | 108 | await runCommand({ command, job: options.job }) |
98 | 109 | ||
99 | await fixHLSPlaylistIfNeeded(options) | 110 | await fixHLSPlaylistIfNeeded(options) |
diff --git a/server/helpers/upload.ts b/server/helpers/upload.ts index 3cb17edd0..f5f476913 100644 --- a/server/helpers/upload.ts +++ b/server/helpers/upload.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { RESUMABLE_UPLOAD_DIRECTORY } from '../initializers/constants' | 2 | import { DIRECTORIES } from '@server/initializers/constants' |
3 | 3 | ||
4 | function getResumableUploadPath (filename?: string) { | 4 | function getResumableUploadPath (filename?: string) { |
5 | if (filename) return join(RESUMABLE_UPLOAD_DIRECTORY, filename) | 5 | if (filename) return join(DIRECTORIES.RESUMABLE_UPLOAD, filename) |
6 | 6 | ||
7 | return RESUMABLE_UPLOAD_DIRECTORY | 7 | return DIRECTORIES.RESUMABLE_UPLOAD |
8 | } | 8 | } |
9 | 9 | ||
10 | // --------------------------------------------------------------------------- | 10 | // --------------------------------------------------------------------------- |
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts index 88bdb16b6..6d87c74f7 100644 --- a/server/helpers/webtorrent.ts +++ b/server/helpers/webtorrent.ts | |||
@@ -164,7 +164,10 @@ function generateMagnetUri ( | |||
164 | ) { | 164 | ) { |
165 | const xs = videoFile.getTorrentUrl() | 165 | const xs = videoFile.getTorrentUrl() |
166 | const announce = trackerUrls | 166 | const announce = trackerUrls |
167 | let urlList = [ videoFile.getFileUrl(video) ] | 167 | |
168 | let urlList = video.requiresAuth(video.uuid) | ||
169 | ? [] | ||
170 | : [ videoFile.getFileUrl(video) ] | ||
168 | 171 | ||
169 | const redundancies = videoFile.RedundancyVideos | 172 | const redundancies = videoFile.RedundancyVideos |
170 | if (isArray(redundancies)) urlList = urlList.concat(redundancies.map(r => r.fileUrl)) | 173 | if (isArray(redundancies)) urlList = urlList.concat(redundancies.map(r => r.fileUrl)) |
@@ -240,6 +243,8 @@ function buildAnnounceList () { | |||
240 | } | 243 | } |
241 | 244 | ||
242 | function buildUrlList (video: MVideo, videoFile: MVideoFile) { | 245 | function buildUrlList (video: MVideo, videoFile: MVideoFile) { |
246 | if (video.requiresAuth(video.uuid)) return [] | ||
247 | |||
243 | return [ videoFile.getFileUrl(video) ] | 248 | return [ videoFile.getFileUrl(video) ] |
244 | } | 249 | } |
245 | 250 | ||
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index cab61948a..88bdd07fe 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -662,10 +662,15 @@ const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = { | |||
662 | // Express static paths (router) | 662 | // Express static paths (router) |
663 | const STATIC_PATHS = { | 663 | const STATIC_PATHS = { |
664 | THUMBNAILS: '/static/thumbnails/', | 664 | THUMBNAILS: '/static/thumbnails/', |
665 | |||
665 | WEBSEED: '/static/webseed/', | 666 | WEBSEED: '/static/webseed/', |
667 | PRIVATE_WEBSEED: '/static/webseed/private/', | ||
668 | |||
666 | REDUNDANCY: '/static/redundancy/', | 669 | REDUNDANCY: '/static/redundancy/', |
670 | |||
667 | STREAMING_PLAYLISTS: { | 671 | STREAMING_PLAYLISTS: { |
668 | HLS: '/static/streaming-playlists/hls' | 672 | HLS: '/static/streaming-playlists/hls', |
673 | PRIVATE_HLS: '/static/streaming-playlists/hls/private/' | ||
669 | } | 674 | } |
670 | } | 675 | } |
671 | const STATIC_DOWNLOAD_PATHS = { | 676 | const STATIC_DOWNLOAD_PATHS = { |
@@ -745,12 +750,32 @@ const LRU_CACHE = { | |||
745 | }, | 750 | }, |
746 | ACTOR_IMAGE_STATIC: { | 751 | ACTOR_IMAGE_STATIC: { |
747 | MAX_SIZE: 500 | 752 | MAX_SIZE: 500 |
753 | }, | ||
754 | STATIC_VIDEO_FILES_RIGHTS_CHECK: { | ||
755 | MAX_SIZE: 5000, | ||
756 | TTL: parseDurationToMs('10 seconds') | ||
757 | }, | ||
758 | VIDEO_TOKENS: { | ||
759 | MAX_SIZE: 100_000, | ||
760 | TTL: parseDurationToMs('8 hours') | ||
748 | } | 761 | } |
749 | } | 762 | } |
750 | 763 | ||
751 | const RESUMABLE_UPLOAD_DIRECTORY = join(CONFIG.STORAGE.TMP_DIR, 'resumable-uploads') | 764 | const DIRECTORIES = { |
752 | const HLS_STREAMING_PLAYLIST_DIRECTORY = join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls') | 765 | RESUMABLE_UPLOAD: join(CONFIG.STORAGE.TMP_DIR, 'resumable-uploads'), |
753 | const HLS_REDUNDANCY_DIRECTORY = join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') | 766 | |
767 | HLS_STREAMING_PLAYLIST: { | ||
768 | PUBLIC: join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls'), | ||
769 | PRIVATE: join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls', 'private') | ||
770 | }, | ||
771 | |||
772 | VIDEOS: { | ||
773 | PUBLIC: CONFIG.STORAGE.VIDEOS_DIR, | ||
774 | PRIVATE: join(CONFIG.STORAGE.VIDEOS_DIR, 'private') | ||
775 | }, | ||
776 | |||
777 | HLS_REDUNDANCY: join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') | ||
778 | } | ||
754 | 779 | ||
755 | const RESUMABLE_UPLOAD_SESSION_LIFETIME = SCHEDULER_INTERVALS_MS.REMOVE_DANGLING_RESUMABLE_UPLOADS | 780 | const RESUMABLE_UPLOAD_SESSION_LIFETIME = SCHEDULER_INTERVALS_MS.REMOVE_DANGLING_RESUMABLE_UPLOADS |
756 | 781 | ||
@@ -971,9 +996,8 @@ export { | |||
971 | PEERTUBE_VERSION, | 996 | PEERTUBE_VERSION, |
972 | LAZY_STATIC_PATHS, | 997 | LAZY_STATIC_PATHS, |
973 | SEARCH_INDEX, | 998 | SEARCH_INDEX, |
974 | RESUMABLE_UPLOAD_DIRECTORY, | 999 | DIRECTORIES, |
975 | RESUMABLE_UPLOAD_SESSION_LIFETIME, | 1000 | RESUMABLE_UPLOAD_SESSION_LIFETIME, |
976 | HLS_REDUNDANCY_DIRECTORY, | ||
977 | P2P_MEDIA_LOADER_PEER_VERSION, | 1001 | P2P_MEDIA_LOADER_PEER_VERSION, |
978 | ACTOR_IMAGES_SIZE, | 1002 | ACTOR_IMAGES_SIZE, |
979 | ACCEPT_HEADERS, | 1003 | ACCEPT_HEADERS, |
@@ -1007,7 +1031,6 @@ export { | |||
1007 | VIDEO_FILTERS, | 1031 | VIDEO_FILTERS, |
1008 | ROUTE_CACHE_LIFETIME, | 1032 | ROUTE_CACHE_LIFETIME, |
1009 | SORTABLE_COLUMNS, | 1033 | SORTABLE_COLUMNS, |
1010 | HLS_STREAMING_PLAYLIST_DIRECTORY, | ||
1011 | JOB_TTL, | 1034 | JOB_TTL, |
1012 | DEFAULT_THEME_NAME, | 1035 | DEFAULT_THEME_NAME, |
1013 | NSFW_POLICY_TYPES, | 1036 | NSFW_POLICY_TYPES, |
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index b02be9567..f5d8eedf1 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts | |||
@@ -10,7 +10,7 @@ import { ApplicationModel } from '../models/application/application' | |||
10 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 10 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
11 | import { applicationExist, clientsExist, usersExist } from './checker-after-init' | 11 | import { applicationExist, clientsExist, usersExist } from './checker-after-init' |
12 | import { CONFIG } from './config' | 12 | import { CONFIG } from './config' |
13 | import { FILES_CACHE, HLS_STREAMING_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION, RESUMABLE_UPLOAD_DIRECTORY } from './constants' | 13 | import { DIRECTORIES, FILES_CACHE, LAST_MIGRATION_VERSION } from './constants' |
14 | import { sequelizeTypescript } from './database' | 14 | import { sequelizeTypescript } from './database' |
15 | 15 | ||
16 | async function installApplication () { | 16 | async function installApplication () { |
@@ -92,11 +92,13 @@ function createDirectoriesIfNotExist () { | |||
92 | tasks.push(ensureDir(dir)) | 92 | tasks.push(ensureDir(dir)) |
93 | } | 93 | } |
94 | 94 | ||
95 | // Playlist directories | 95 | tasks.push(ensureDir(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE)) |
96 | tasks.push(ensureDir(HLS_STREAMING_PLAYLIST_DIRECTORY)) | 96 | tasks.push(ensureDir(DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC)) |
97 | tasks.push(ensureDir(DIRECTORIES.VIDEOS.PUBLIC)) | ||
98 | tasks.push(ensureDir(DIRECTORIES.VIDEOS.PRIVATE)) | ||
97 | 99 | ||
98 | // Resumable upload directory | 100 | // Resumable upload directory |
99 | tasks.push(ensureDir(RESUMABLE_UPLOAD_DIRECTORY)) | 101 | tasks.push(ensureDir(DIRECTORIES.RESUMABLE_UPLOAD)) |
100 | 102 | ||
101 | return Promise.all(tasks) | 103 | return Promise.all(tasks) |
102 | } | 104 | } |
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts index 35b05ec5a..bc0d4301f 100644 --- a/server/lib/auth/oauth.ts +++ b/server/lib/auth/oauth.ts | |||
@@ -95,14 +95,9 @@ async function handleOAuthToken (req: express.Request, options: { refreshTokenAu | |||
95 | 95 | ||
96 | function handleOAuthAuthenticate ( | 96 | function handleOAuthAuthenticate ( |
97 | req: express.Request, | 97 | req: express.Request, |
98 | res: express.Response, | 98 | res: express.Response |
99 | authenticateInQuery = false | ||
100 | ) { | 99 | ) { |
101 | const options = authenticateInQuery | 100 | return oAuthServer.authenticate(new Request(req), new Response(res)) |
102 | ? { allowBearerTokensInQueryString: true } | ||
103 | : {} | ||
104 | |||
105 | return oAuthServer.authenticate(new Request(req), new Response(res), options) | ||
106 | } | 101 | } |
107 | 102 | ||
108 | export { | 103 | export { |
diff --git a/server/lib/job-queue/handlers/manage-video-torrent.ts b/server/lib/job-queue/handlers/manage-video-torrent.ts index 03aa414c9..425915c96 100644 --- a/server/lib/job-queue/handlers/manage-video-torrent.ts +++ b/server/lib/job-queue/handlers/manage-video-torrent.ts | |||
@@ -82,7 +82,7 @@ async function loadStreamingPlaylistOrLog (streamingPlaylistId: number) { | |||
82 | async function loadFileOrLog (videoFileId: number) { | 82 | async function loadFileOrLog (videoFileId: number) { |
83 | if (!videoFileId) return undefined | 83 | if (!videoFileId) return undefined |
84 | 84 | ||
85 | const file = await VideoFileModel.loadWithVideo(videoFileId) | 85 | const file = await VideoFileModel.load(videoFileId) |
86 | 86 | ||
87 | if (!file) { | 87 | if (!file) { |
88 | logger.debug('Do not process torrent for file %d: does not exist anymore.', videoFileId) | 88 | logger.debug('Do not process torrent for file %d: does not exist anymore.', videoFileId) |
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 28c3d325d..0b68555d1 100644 --- a/server/lib/job-queue/handlers/move-to-object-storage.ts +++ b/server/lib/job-queue/handlers/move-to-object-storage.ts | |||
@@ -3,10 +3,10 @@ import { remove } from 'fs-extra' | |||
3 | import { join } from 'path' | 3 | 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 { CONFIG } from '@server/initializers/config' | ||
7 | import { P2P_MEDIA_LOADER_PEER_VERSION } from '@server/initializers/constants' | 6 | import { P2P_MEDIA_LOADER_PEER_VERSION } from '@server/initializers/constants' |
8 | import { storeHLSFileFromFilename, storeWebTorrentFile } from '@server/lib/object-storage' | 7 | import { storeHLSFileFromFilename, storeWebTorrentFile } from '@server/lib/object-storage' |
9 | import { getHLSDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' | 8 | import { getHLSDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' |
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' |
11 | import { VideoModel } from '@server/models/video/video' | 11 | import { VideoModel } from '@server/models/video/video' |
12 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' | 12 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' |
@@ -72,9 +72,9 @@ async function moveWebTorrentFiles (video: MVideoWithAllFiles) { | |||
72 | for (const file of video.VideoFiles) { | 72 | for (const file of video.VideoFiles) { |
73 | if (file.storage !== VideoStorage.FILE_SYSTEM) continue | 73 | if (file.storage !== VideoStorage.FILE_SYSTEM) continue |
74 | 74 | ||
75 | const fileUrl = await storeWebTorrentFile(file.filename) | 75 | const fileUrl = await storeWebTorrentFile(video, file) |
76 | 76 | ||
77 | const oldPath = join(CONFIG.STORAGE.VIDEOS_DIR, file.filename) | 77 | const oldPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, file) |
78 | await onFileMoved({ videoOrPlaylist: video, file, fileUrl, oldPath }) | 78 | await onFileMoved({ videoOrPlaylist: video, file, fileUrl, oldPath }) |
79 | } | 79 | } |
80 | } | 80 | } |
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 7dbffc955..c6263f55a 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -18,6 +18,7 @@ import { VideoStreamingPlaylistModel } from '@server/models/video/video-streamin | |||
18 | import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@server/types/models' | 18 | import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@server/types/models' |
19 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' | 19 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' |
20 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | 20 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
21 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
21 | 22 | ||
22 | const lTags = loggerTagsFactory('live', 'job') | 23 | const lTags = loggerTagsFactory('live', 'job') |
23 | 24 | ||
@@ -205,18 +206,27 @@ async function assignReplayFilesToVideo (options: { | |||
205 | const concatenatedTsFiles = await readdir(replayDirectory) | 206 | const concatenatedTsFiles = await readdir(replayDirectory) |
206 | 207 | ||
207 | for (const concatenatedTsFile of concatenatedTsFiles) { | 208 | for (const concatenatedTsFile of concatenatedTsFiles) { |
209 | const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) | ||
210 | |||
208 | const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile) | 211 | const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile) |
209 | 212 | ||
210 | const probe = await ffprobePromise(concatenatedTsFilePath) | 213 | const probe = await ffprobePromise(concatenatedTsFilePath) |
211 | const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) | 214 | const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) |
212 | const { resolution } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe) | 215 | const { resolution } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe) |
213 | 216 | ||
214 | await generateHlsPlaylistResolutionFromTS({ | 217 | try { |
215 | video, | 218 | await generateHlsPlaylistResolutionFromTS({ |
216 | concatenatedTsFilePath, | 219 | video, |
217 | resolution, | 220 | inputFileMutexReleaser, |
218 | isAAC: audioStream?.codec_name === 'aac' | 221 | concatenatedTsFilePath, |
219 | }) | 222 | resolution, |
223 | isAAC: audioStream?.codec_name === 'aac' | ||
224 | }) | ||
225 | } catch (err) { | ||
226 | logger.error('Cannot generate HLS playlist resolution from TS files.', { err }) | ||
227 | } | ||
228 | |||
229 | inputFileMutexReleaser() | ||
220 | } | 230 | } |
221 | 231 | ||
222 | return video | 232 | return video |
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index b0e92acf7..48c675678 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts | |||
@@ -94,15 +94,24 @@ async function handleHLSJob (job: Job, payload: HLSTranscodingPayload, video: MV | |||
94 | 94 | ||
95 | const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist() | 95 | const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist() |
96 | 96 | ||
97 | await VideoPathManager.Instance.makeAvailableVideoFile(videoFileInput.withVideoOrPlaylist(videoOrStreamingPlaylist), videoInputPath => { | 97 | const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
98 | return generateHlsPlaylistResolution({ | 98 | |
99 | video, | 99 | try { |
100 | videoInputPath, | 100 | await videoFileInput.getVideo().reload() |
101 | resolution: payload.resolution, | 101 | |
102 | copyCodecs: payload.copyCodecs, | 102 | await VideoPathManager.Instance.makeAvailableVideoFile(videoFileInput.withVideoOrPlaylist(videoOrStreamingPlaylist), videoInputPath => { |
103 | job | 103 | return generateHlsPlaylistResolution({ |
104 | video, | ||
105 | videoInputPath, | ||
106 | inputFileMutexReleaser, | ||
107 | resolution: payload.resolution, | ||
108 | copyCodecs: payload.copyCodecs, | ||
109 | job | ||
110 | }) | ||
104 | }) | 111 | }) |
105 | }) | 112 | } finally { |
113 | inputFileMutexReleaser() | ||
114 | } | ||
106 | 115 | ||
107 | logger.info('HLS transcoding job for %s ended.', video.uuid, lTags(video.uuid)) | 116 | logger.info('HLS transcoding job for %s ended.', video.uuid, lTags(video.uuid)) |
108 | 117 | ||
@@ -177,38 +186,44 @@ async function onVideoFirstWebTorrentTranscoding ( | |||
177 | transcodeType: TranscodeVODOptionsType, | 186 | transcodeType: TranscodeVODOptionsType, |
178 | user: MUserId | 187 | user: MUserId |
179 | ) { | 188 | ) { |
180 | const { resolution, audioStream } = await videoArg.probeMaxQualityFile() | 189 | const mutexReleaser = await VideoPathManager.Instance.lockFiles(videoArg.uuid) |
181 | 190 | ||
182 | // Maybe the video changed in database, refresh it | 191 | try { |
183 | const videoDatabase = await VideoModel.loadFull(videoArg.uuid) | 192 | // Maybe the video changed in database, refresh it |
184 | // Video does not exist anymore | 193 | const videoDatabase = await VideoModel.loadFull(videoArg.uuid) |
185 | if (!videoDatabase) return undefined | 194 | // Video does not exist anymore |
186 | 195 | if (!videoDatabase) return undefined | |
187 | // Generate HLS version of the original file | 196 | |
188 | const originalFileHLSPayload = { | 197 | const { resolution, audioStream } = await videoDatabase.probeMaxQualityFile() |
189 | ...payload, | 198 | |
190 | 199 | // Generate HLS version of the original file | |
191 | hasAudio: !!audioStream, | 200 | const originalFileHLSPayload = { |
192 | resolution: videoDatabase.getMaxQualityFile().resolution, | 201 | ...payload, |
193 | // If we quick transcoded original file, force transcoding for HLS to avoid some weird playback issues | 202 | |
194 | copyCodecs: transcodeType !== 'quick-transcode', | 203 | hasAudio: !!audioStream, |
195 | isMaxQuality: true | 204 | resolution: videoDatabase.getMaxQualityFile().resolution, |
196 | } | 205 | // If we quick transcoded original file, force transcoding for HLS to avoid some weird playback issues |
197 | const hasHls = await createHlsJobIfEnabled(user, originalFileHLSPayload) | 206 | copyCodecs: transcodeType !== 'quick-transcode', |
198 | const hasNewResolutions = await createLowerResolutionsJobs({ | 207 | isMaxQuality: true |
199 | video: videoDatabase, | 208 | } |
200 | user, | 209 | const hasHls = await createHlsJobIfEnabled(user, originalFileHLSPayload) |
201 | videoFileResolution: resolution, | 210 | const hasNewResolutions = await createLowerResolutionsJobs({ |
202 | hasAudio: !!audioStream, | 211 | video: videoDatabase, |
203 | type: 'webtorrent', | 212 | user, |
204 | isNewVideo: payload.isNewVideo ?? true | 213 | videoFileResolution: resolution, |
205 | }) | 214 | hasAudio: !!audioStream, |
206 | 215 | type: 'webtorrent', | |
207 | await VideoJobInfoModel.decrease(videoDatabase.uuid, 'pendingTranscode') | 216 | isNewVideo: payload.isNewVideo ?? true |
208 | 217 | }) | |
209 | // Move to next state if there are no other resolutions to generate | 218 | |
210 | if (!hasHls && !hasNewResolutions) { | 219 | await VideoJobInfoModel.decrease(videoDatabase.uuid, 'pendingTranscode') |
211 | await retryTransactionWrapper(moveToNextState, { video: videoDatabase, isNewVideo: payload.isNewVideo }) | 220 | |
221 | // Move to next state if there are no other resolutions to generate | ||
222 | if (!hasHls && !hasNewResolutions) { | ||
223 | await retryTransactionWrapper(moveToNextState, { video: videoDatabase, isNewVideo: payload.isNewVideo }) | ||
224 | } | ||
225 | } finally { | ||
226 | mutexReleaser() | ||
212 | } | 227 | } |
213 | } | 228 | } |
214 | 229 | ||
diff --git a/server/lib/object-storage/videos.ts b/server/lib/object-storage/videos.ts index 62aae248b..e323baaa2 100644 --- a/server/lib/object-storage/videos.ts +++ b/server/lib/object-storage/videos.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { basename, join } from 'path' | 1 | import { basename, join } from 'path' |
2 | import { logger } from '@server/helpers/logger' | 2 | import { logger } from '@server/helpers/logger' |
3 | import { CONFIG } from '@server/initializers/config' | 3 | import { CONFIG } from '@server/initializers/config' |
4 | import { MStreamingPlaylistVideo, 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 { generateHLSObjectBaseStorageKey, generateHLSObjectStorageKey, generateWebTorrentObjectStorageKey } from './keys' | 7 | import { generateHLSObjectBaseStorageKey, generateHLSObjectStorageKey, generateWebTorrentObjectStorageKey } from './keys' |
7 | import { listKeysOfPrefix, lTags, makeAvailable, removeObject, removePrefix, storeObject } from './shared' | 8 | import { listKeysOfPrefix, lTags, makeAvailable, removeObject, removePrefix, storeObject } from './shared' |
8 | 9 | ||
@@ -30,10 +31,10 @@ function storeHLSFileFromPath (playlist: MStreamingPlaylistVideo, path: string) | |||
30 | 31 | ||
31 | // --------------------------------------------------------------------------- | 32 | // --------------------------------------------------------------------------- |
32 | 33 | ||
33 | function storeWebTorrentFile (filename: string) { | 34 | function storeWebTorrentFile (video: MVideo, file: MVideoFile) { |
34 | return storeObject({ | 35 | return storeObject({ |
35 | inputPath: join(CONFIG.STORAGE.VIDEOS_DIR, filename), | 36 | inputPath: VideoPathManager.Instance.getFSVideoFileOutputPath(video, file), |
36 | objectStorageKey: generateWebTorrentObjectStorageKey(filename), | 37 | objectStorageKey: generateWebTorrentObjectStorageKey(file.filename), |
37 | bucketInfo: CONFIG.OBJECT_STORAGE.VIDEOS | 38 | bucketInfo: CONFIG.OBJECT_STORAGE.VIDEOS |
38 | }) | 39 | }) |
39 | } | 40 | } |
diff --git a/server/lib/paths.ts b/server/lib/paths.ts index b29854700..470970f55 100644 --- a/server/lib/paths.ts +++ b/server/lib/paths.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { CONFIG } from '@server/initializers/config' | 2 | import { CONFIG } from '@server/initializers/config' |
3 | import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY, VIDEO_LIVE } from '@server/initializers/constants' | 3 | import { DIRECTORIES, VIDEO_LIVE } from '@server/initializers/constants' |
4 | import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models' | 4 | import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models' |
5 | import { removeFragmentedMP4Ext } from '@shared/core-utils' | 5 | import { removeFragmentedMP4Ext } from '@shared/core-utils' |
6 | import { buildUUID } from '@shared/extra-utils' | 6 | import { buildUUID } from '@shared/extra-utils' |
7 | import { isVideoInPrivateDirectory } from './video-privacy' | ||
7 | 8 | ||
8 | // ################## Video file name ################## | 9 | // ################## Video file name ################## |
9 | 10 | ||
@@ -17,20 +18,24 @@ function generateHLSVideoFilename (resolution: number) { | |||
17 | 18 | ||
18 | // ################## Streaming playlist ################## | 19 | // ################## Streaming playlist ################## |
19 | 20 | ||
20 | function getLiveDirectory (video: MVideoUUID) { | 21 | function getLiveDirectory (video: MVideo) { |
21 | return getHLSDirectory(video) | 22 | return getHLSDirectory(video) |
22 | } | 23 | } |
23 | 24 | ||
24 | function getLiveReplayBaseDirectory (video: MVideoUUID) { | 25 | function getLiveReplayBaseDirectory (video: MVideo) { |
25 | return join(getLiveDirectory(video), VIDEO_LIVE.REPLAY_DIRECTORY) | 26 | return join(getLiveDirectory(video), VIDEO_LIVE.REPLAY_DIRECTORY) |
26 | } | 27 | } |
27 | 28 | ||
28 | function getHLSDirectory (video: MVideoUUID) { | 29 | function getHLSDirectory (video: MVideo) { |
29 | return join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) | 30 | if (isVideoInPrivateDirectory(video.privacy)) { |
31 | return join(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, video.uuid) | ||
32 | } | ||
33 | |||
34 | return join(DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC, video.uuid) | ||
30 | } | 35 | } |
31 | 36 | ||
32 | function getHLSRedundancyDirectory (video: MVideoUUID) { | 37 | function getHLSRedundancyDirectory (video: MVideoUUID) { |
33 | return join(HLS_REDUNDANCY_DIRECTORY, video.uuid) | 38 | return join(DIRECTORIES.HLS_REDUNDANCY, video.uuid) |
34 | } | 39 | } |
35 | 40 | ||
36 | function getHlsResolutionPlaylistFilename (videoFilename: string) { | 41 | function getHlsResolutionPlaylistFilename (videoFilename: string) { |
diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts index 5bfbc3cd2..30bf189db 100644 --- a/server/lib/schedulers/update-videos-scheduler.ts +++ b/server/lib/schedulers/update-videos-scheduler.ts | |||
@@ -1,11 +1,14 @@ | |||
1 | import { VideoModel } from '@server/models/video/video' | 1 | import { VideoModel } from '@server/models/video/video' |
2 | import { MVideoFullLight } from '@server/types/models' | 2 | import { MScheduleVideoUpdate } from '@server/types/models' |
3 | import { VideoPrivacy, VideoState } from '@shared/models' | ||
3 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
4 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' | 5 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' |
5 | import { sequelizeTypescript } from '../../initializers/database' | 6 | import { sequelizeTypescript } from '../../initializers/database' |
6 | import { ScheduleVideoUpdateModel } from '../../models/video/schedule-video-update' | 7 | import { ScheduleVideoUpdateModel } from '../../models/video/schedule-video-update' |
7 | import { federateVideoIfNeeded } from '../activitypub/videos' | ||
8 | import { Notifier } from '../notifier' | 8 | import { Notifier } from '../notifier' |
9 | import { addVideoJobsAfterUpdate } from '../video' | ||
10 | import { VideoPathManager } from '../video-path-manager' | ||
11 | import { setVideoPrivacy } from '../video-privacy' | ||
9 | import { AbstractScheduler } from './abstract-scheduler' | 12 | import { AbstractScheduler } from './abstract-scheduler' |
10 | 13 | ||
11 | export class UpdateVideosScheduler extends AbstractScheduler { | 14 | export class UpdateVideosScheduler extends AbstractScheduler { |
@@ -26,35 +29,54 @@ export class UpdateVideosScheduler extends AbstractScheduler { | |||
26 | if (!await ScheduleVideoUpdateModel.areVideosToUpdate()) return undefined | 29 | if (!await ScheduleVideoUpdateModel.areVideosToUpdate()) return undefined |
27 | 30 | ||
28 | const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate() | 31 | const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate() |
29 | const publishedVideos: MVideoFullLight[] = [] | ||
30 | 32 | ||
31 | for (const schedule of schedules) { | 33 | for (const schedule of schedules) { |
32 | await sequelizeTypescript.transaction(async t => { | 34 | const videoOnly = await VideoModel.load(schedule.videoId) |
33 | const video = await VideoModel.loadFull(schedule.videoId, t) | 35 | const mutexReleaser = await VideoPathManager.Instance.lockFiles(videoOnly.uuid) |
34 | 36 | ||
35 | logger.info('Executing scheduled video update on %s.', video.uuid) | 37 | try { |
38 | const { video, published } = await this.updateAVideo(schedule) | ||
36 | 39 | ||
37 | if (schedule.privacy) { | 40 | if (published) Notifier.Instance.notifyOnVideoPublishedAfterScheduledUpdate(video) |
38 | const wasConfidentialVideo = video.isConfidential() | 41 | } catch (err) { |
39 | const isNewVideo = video.isNewVideo(schedule.privacy) | 42 | logger.error('Cannot update video', { err }) |
43 | } | ||
40 | 44 | ||
41 | video.setPrivacy(schedule.privacy) | 45 | mutexReleaser() |
42 | await video.save({ transaction: t }) | 46 | } |
43 | await federateVideoIfNeeded(video, isNewVideo, t) | 47 | } |
48 | |||
49 | private async updateAVideo (schedule: MScheduleVideoUpdate) { | ||
50 | let oldPrivacy: VideoPrivacy | ||
51 | let isNewVideo: boolean | ||
52 | let published = false | ||
53 | |||
54 | const video = await sequelizeTypescript.transaction(async t => { | ||
55 | const video = await VideoModel.loadFull(schedule.videoId, t) | ||
56 | if (video.state === VideoState.TO_TRANSCODE) return | ||
57 | |||
58 | logger.info('Executing scheduled video update on %s.', video.uuid) | ||
59 | |||
60 | if (schedule.privacy) { | ||
61 | isNewVideo = video.isNewVideo(schedule.privacy) | ||
62 | oldPrivacy = video.privacy | ||
44 | 63 | ||
45 | if (wasConfidentialVideo) { | 64 | setVideoPrivacy(video, schedule.privacy) |
46 | publishedVideos.push(video) | 65 | await video.save({ transaction: t }) |
47 | } | 66 | |
67 | if (oldPrivacy === VideoPrivacy.PRIVATE) { | ||
68 | published = true | ||
48 | } | 69 | } |
70 | } | ||
49 | 71 | ||
50 | await schedule.destroy({ transaction: t }) | 72 | await schedule.destroy({ transaction: t }) |
51 | }) | ||
52 | } | ||
53 | 73 | ||
54 | for (const v of publishedVideos) { | 74 | return video |
55 | Notifier.Instance.notifyOnNewVideoIfNeeded(v) | 75 | }) |
56 | Notifier.Instance.notifyOnVideoPublishedAfterScheduledUpdate(v) | 76 | |
57 | } | 77 | await addVideoJobsAfterUpdate({ video, oldPrivacy, isNewVideo, nameChanged: false }) |
78 | |||
79 | return { video, published } | ||
58 | } | 80 | } |
59 | 81 | ||
60 | static get Instance () { | 82 | static get Instance () { |
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts index 91c217615..78245fa6a 100644 --- a/server/lib/schedulers/videos-redundancy-scheduler.ts +++ b/server/lib/schedulers/videos-redundancy-scheduler.ts | |||
@@ -16,7 +16,7 @@ import { VideosRedundancyStrategy } from '../../../shared/models/redundancy' | |||
16 | import { logger, loggerTagsFactory } from '../../helpers/logger' | 16 | import { logger, loggerTagsFactory } from '../../helpers/logger' |
17 | import { downloadWebTorrentVideo } from '../../helpers/webtorrent' | 17 | import { downloadWebTorrentVideo } from '../../helpers/webtorrent' |
18 | import { CONFIG } from '../../initializers/config' | 18 | import { CONFIG } from '../../initializers/config' |
19 | import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../../initializers/constants' | 19 | import { DIRECTORIES, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../../initializers/constants' |
20 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 20 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
21 | import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' | 21 | import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' |
22 | import { getLocalVideoCacheFileActivityPubUrl, getLocalVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url' | 22 | import { getLocalVideoCacheFileActivityPubUrl, getLocalVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url' |
@@ -262,7 +262,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
262 | 262 | ||
263 | logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, strategy, lTags(video.uuid)) | 263 | logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, strategy, lTags(video.uuid)) |
264 | 264 | ||
265 | const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid) | 265 | const destDirectory = join(DIRECTORIES.HLS_REDUNDANCY, video.uuid) |
266 | const masterPlaylistUrl = playlist.getMasterPlaylistUrl(video) | 266 | const masterPlaylistUrl = playlist.getMasterPlaylistUrl(video) |
267 | 267 | ||
268 | const maxSizeKB = this.getTotalFileSizes([], [ playlist ]) / 1000 | 268 | const maxSizeKB = this.getTotalFileSizes([], [ playlist ]) / 1000 |
diff --git a/server/lib/transcoding/transcoding.ts b/server/lib/transcoding/transcoding.ts index 44e26754d..736e96e65 100644 --- a/server/lib/transcoding/transcoding.ts +++ b/server/lib/transcoding/transcoding.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { MutexInterface } from 'async-mutex' | ||
1 | import { Job } from 'bullmq' | 2 | import { Job } from 'bullmq' |
2 | import { copyFile, ensureDir, move, remove, stat } from 'fs-extra' | 3 | import { copyFile, ensureDir, move, remove, stat } from 'fs-extra' |
3 | import { basename, extname as extnameUtil, join } from 'path' | 4 | import { basename, extname as extnameUtil, join } from 'path' |
@@ -6,11 +7,13 @@ import { retryTransactionWrapper } from '@server/helpers/database-utils' | |||
6 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 7 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
7 | import { sequelizeTypescript } from '@server/initializers/database' | 8 | import { sequelizeTypescript } from '@server/initializers/database' |
8 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 9 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
10 | import { pick } from '@shared/core-utils' | ||
9 | import { VideoResolution, VideoStorage } from '../../../shared/models/videos' | 11 | import { VideoResolution, VideoStorage } from '../../../shared/models/videos' |
10 | import { | 12 | import { |
11 | buildFileMetadata, | 13 | buildFileMetadata, |
12 | canDoQuickTranscode, | 14 | canDoQuickTranscode, |
13 | computeResolutionsToTranscode, | 15 | computeResolutionsToTranscode, |
16 | ffprobePromise, | ||
14 | getVideoStreamDuration, | 17 | getVideoStreamDuration, |
15 | getVideoStreamFPS, | 18 | getVideoStreamFPS, |
16 | transcodeVOD, | 19 | transcodeVOD, |
@@ -33,7 +36,7 @@ import { VideoTranscodingProfilesManager } from './default-transcoding-profiles' | |||
33 | */ | 36 | */ |
34 | 37 | ||
35 | // Optimize the original video file and replace it. The resolution is not changed. | 38 | // Optimize the original video file and replace it. The resolution is not changed. |
36 | function optimizeOriginalVideofile (options: { | 39 | async function optimizeOriginalVideofile (options: { |
37 | video: MVideoFullLight | 40 | video: MVideoFullLight |
38 | inputVideoFile: MVideoFile | 41 | inputVideoFile: MVideoFile |
39 | job: Job | 42 | job: Job |
@@ -43,49 +46,61 @@ function optimizeOriginalVideofile (options: { | |||
43 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 46 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
44 | const newExtname = '.mp4' | 47 | const newExtname = '.mp4' |
45 | 48 | ||
46 | return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async videoInputPath => { | 49 | const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
47 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | ||
48 | 50 | ||
49 | const transcodeType: TranscodeVODOptionsType = await canDoQuickTranscode(videoInputPath) | 51 | try { |
50 | ? 'quick-transcode' | 52 | await video.reload() |
51 | : 'video' | ||
52 | 53 | ||
53 | const resolution = buildOriginalFileResolution(inputVideoFile.resolution) | 54 | const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video) |
54 | 55 | ||
55 | const transcodeOptions: TranscodeVODOptions = { | 56 | const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async videoInputPath => { |
56 | type: transcodeType, | 57 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) |
57 | 58 | ||
58 | inputPath: videoInputPath, | 59 | const transcodeType: TranscodeVODOptionsType = await canDoQuickTranscode(videoInputPath) |
59 | outputPath: videoTranscodedPath, | 60 | ? 'quick-transcode' |
61 | : 'video' | ||
60 | 62 | ||
61 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 63 | const resolution = buildOriginalFileResolution(inputVideoFile.resolution) |
62 | profile: CONFIG.TRANSCODING.PROFILE, | ||
63 | 64 | ||
64 | resolution, | 65 | const transcodeOptions: TranscodeVODOptions = { |
66 | type: transcodeType, | ||
65 | 67 | ||
66 | job | 68 | inputPath: videoInputPath, |
67 | } | 69 | outputPath: videoTranscodedPath, |
68 | 70 | ||
69 | // Could be very long! | 71 | inputFileMutexReleaser, |
70 | await transcodeVOD(transcodeOptions) | ||
71 | 72 | ||
72 | // Important to do this before getVideoFilename() to take in account the new filename | 73 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
73 | inputVideoFile.resolution = resolution | 74 | profile: CONFIG.TRANSCODING.PROFILE, |
74 | inputVideoFile.extname = newExtname | ||
75 | inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname) | ||
76 | inputVideoFile.storage = VideoStorage.FILE_SYSTEM | ||
77 | 75 | ||
78 | const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile) | 76 | resolution, |
79 | 77 | ||
80 | const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) | 78 | job |
81 | await remove(videoInputPath) | 79 | } |
82 | 80 | ||
83 | return { transcodeType, videoFile } | 81 | // Could be very long! |
84 | }) | 82 | await transcodeVOD(transcodeOptions) |
83 | |||
84 | // Important to do this before getVideoFilename() to take in account the new filename | ||
85 | inputVideoFile.resolution = resolution | ||
86 | inputVideoFile.extname = newExtname | ||
87 | inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname) | ||
88 | inputVideoFile.storage = VideoStorage.FILE_SYSTEM | ||
89 | |||
90 | const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, inputVideoFile) | ||
91 | await remove(videoInputPath) | ||
92 | |||
93 | return { transcodeType, videoFile } | ||
94 | }) | ||
95 | |||
96 | return result | ||
97 | } finally { | ||
98 | inputFileMutexReleaser() | ||
99 | } | ||
85 | } | 100 | } |
86 | 101 | ||
87 | // Transcode the original video file to a lower resolution compatible with WebTorrent | 102 | // Transcode the original video file to a lower resolution compatible with WebTorrent |
88 | function transcodeNewWebTorrentResolution (options: { | 103 | async function transcodeNewWebTorrentResolution (options: { |
89 | video: MVideoFullLight | 104 | video: MVideoFullLight |
90 | resolution: VideoResolution | 105 | resolution: VideoResolution |
91 | job: Job | 106 | job: Job |
@@ -95,53 +110,68 @@ function transcodeNewWebTorrentResolution (options: { | |||
95 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 110 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
96 | const newExtname = '.mp4' | 111 | const newExtname = '.mp4' |
97 | 112 | ||
98 | return VideoPathManager.Instance.makeAvailableVideoFile(video.getMaxQualityFile().withVideoOrPlaylist(video), async videoInputPath => { | 113 | const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
99 | const newVideoFile = new VideoFileModel({ | ||
100 | resolution, | ||
101 | extname: newExtname, | ||
102 | filename: generateWebTorrentVideoFilename(resolution, newExtname), | ||
103 | size: 0, | ||
104 | videoId: video.id | ||
105 | }) | ||
106 | 114 | ||
107 | const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile) | 115 | try { |
108 | const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename) | 116 | await video.reload() |
109 | 117 | ||
110 | const transcodeOptions = resolution === VideoResolution.H_NOVIDEO | 118 | const file = video.getMaxQualityFile().withVideoOrPlaylist(video) |
111 | ? { | ||
112 | type: 'only-audio' as 'only-audio', | ||
113 | 119 | ||
114 | inputPath: videoInputPath, | 120 | const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => { |
115 | outputPath: videoTranscodedPath, | 121 | const newVideoFile = new VideoFileModel({ |
122 | resolution, | ||
123 | extname: newExtname, | ||
124 | filename: generateWebTorrentVideoFilename(resolution, newExtname), | ||
125 | size: 0, | ||
126 | videoId: video.id | ||
127 | }) | ||
116 | 128 | ||
117 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 129 | const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename) |
118 | profile: CONFIG.TRANSCODING.PROFILE, | ||
119 | 130 | ||
120 | resolution, | 131 | const transcodeOptions = resolution === VideoResolution.H_NOVIDEO |
132 | ? { | ||
133 | type: 'only-audio' as 'only-audio', | ||
121 | 134 | ||
122 | job | 135 | inputPath: videoInputPath, |
123 | } | 136 | outputPath: videoTranscodedPath, |
124 | : { | ||
125 | type: 'video' as 'video', | ||
126 | inputPath: videoInputPath, | ||
127 | outputPath: videoTranscodedPath, | ||
128 | 137 | ||
129 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 138 | inputFileMutexReleaser, |
130 | profile: CONFIG.TRANSCODING.PROFILE, | ||
131 | 139 | ||
132 | resolution, | 140 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
141 | profile: CONFIG.TRANSCODING.PROFILE, | ||
133 | 142 | ||
134 | job | 143 | resolution, |
135 | } | ||
136 | 144 | ||
137 | await transcodeVOD(transcodeOptions) | 145 | job |
146 | } | ||
147 | : { | ||
148 | type: 'video' as 'video', | ||
149 | inputPath: videoInputPath, | ||
150 | outputPath: videoTranscodedPath, | ||
138 | 151 | ||
139 | return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) | 152 | inputFileMutexReleaser, |
140 | }) | 153 | |
154 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | ||
155 | profile: CONFIG.TRANSCODING.PROFILE, | ||
156 | |||
157 | resolution, | ||
158 | |||
159 | job | ||
160 | } | ||
161 | |||
162 | await transcodeVOD(transcodeOptions) | ||
163 | |||
164 | return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, newVideoFile) | ||
165 | }) | ||
166 | |||
167 | return result | ||
168 | } finally { | ||
169 | inputFileMutexReleaser() | ||
170 | } | ||
141 | } | 171 | } |
142 | 172 | ||
143 | // Merge an image with an audio file to create a video | 173 | // Merge an image with an audio file to create a video |
144 | function mergeAudioVideofile (options: { | 174 | async function mergeAudioVideofile (options: { |
145 | video: MVideoFullLight | 175 | video: MVideoFullLight |
146 | resolution: VideoResolution | 176 | resolution: VideoResolution |
147 | job: Job | 177 | job: Job |
@@ -151,54 +181,67 @@ function mergeAudioVideofile (options: { | |||
151 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 181 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
152 | const newExtname = '.mp4' | 182 | const newExtname = '.mp4' |
153 | 183 | ||
154 | const inputVideoFile = video.getMinQualityFile() | 184 | const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
155 | 185 | ||
156 | return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async audioInputPath => { | 186 | try { |
157 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | 187 | await video.reload() |
158 | 188 | ||
159 | // If the user updates the video preview during transcoding | 189 | const inputVideoFile = video.getMinQualityFile() |
160 | const previewPath = video.getPreview().getPath() | ||
161 | const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath)) | ||
162 | await copyFile(previewPath, tmpPreviewPath) | ||
163 | 190 | ||
164 | const transcodeOptions = { | 191 | const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video) |
165 | type: 'merge-audio' as 'merge-audio', | ||
166 | 192 | ||
167 | inputPath: tmpPreviewPath, | 193 | const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async audioInputPath => { |
168 | outputPath: videoTranscodedPath, | 194 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) |
169 | 195 | ||
170 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 196 | // If the user updates the video preview during transcoding |
171 | profile: CONFIG.TRANSCODING.PROFILE, | 197 | const previewPath = video.getPreview().getPath() |
198 | const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath)) | ||
199 | await copyFile(previewPath, tmpPreviewPath) | ||
172 | 200 | ||
173 | audioPath: audioInputPath, | 201 | const transcodeOptions = { |
174 | resolution, | 202 | type: 'merge-audio' as 'merge-audio', |
175 | 203 | ||
176 | job | 204 | inputPath: tmpPreviewPath, |
177 | } | 205 | outputPath: videoTranscodedPath, |
178 | 206 | ||
179 | try { | 207 | inputFileMutexReleaser, |
180 | await transcodeVOD(transcodeOptions) | ||
181 | 208 | ||
182 | await remove(audioInputPath) | 209 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
183 | await remove(tmpPreviewPath) | 210 | profile: CONFIG.TRANSCODING.PROFILE, |
184 | } catch (err) { | ||
185 | await remove(tmpPreviewPath) | ||
186 | throw err | ||
187 | } | ||
188 | 211 | ||
189 | // Important to do this before getVideoFilename() to take in account the new file extension | 212 | audioPath: audioInputPath, |
190 | inputVideoFile.extname = newExtname | 213 | resolution, |
191 | inputVideoFile.resolution = resolution | ||
192 | inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname) | ||
193 | 214 | ||
194 | const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile) | 215 | job |
195 | // ffmpeg generated a new video file, so update the video duration | 216 | } |
196 | // See https://trac.ffmpeg.org/ticket/5456 | ||
197 | video.duration = await getVideoStreamDuration(videoTranscodedPath) | ||
198 | await video.save() | ||
199 | 217 | ||
200 | return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) | 218 | try { |
201 | }) | 219 | await transcodeVOD(transcodeOptions) |
220 | |||
221 | await remove(audioInputPath) | ||
222 | await remove(tmpPreviewPath) | ||
223 | } catch (err) { | ||
224 | await remove(tmpPreviewPath) | ||
225 | throw err | ||
226 | } | ||
227 | |||
228 | // Important to do this before getVideoFilename() to take in account the new file extension | ||
229 | inputVideoFile.extname = newExtname | ||
230 | inputVideoFile.resolution = resolution | ||
231 | inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname) | ||
232 | |||
233 | // ffmpeg generated a new video file, so update the video duration | ||
234 | // See https://trac.ffmpeg.org/ticket/5456 | ||
235 | video.duration = await getVideoStreamDuration(videoTranscodedPath) | ||
236 | await video.save() | ||
237 | |||
238 | return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, inputVideoFile) | ||
239 | }) | ||
240 | |||
241 | return result | ||
242 | } finally { | ||
243 | inputFileMutexReleaser() | ||
244 | } | ||
202 | } | 245 | } |
203 | 246 | ||
204 | // Concat TS segments from a live video to a fragmented mp4 HLS playlist | 247 | // Concat TS segments from a live video to a fragmented mp4 HLS playlist |
@@ -207,13 +250,13 @@ async function generateHlsPlaylistResolutionFromTS (options: { | |||
207 | concatenatedTsFilePath: string | 250 | concatenatedTsFilePath: string |
208 | resolution: VideoResolution | 251 | resolution: VideoResolution |
209 | isAAC: boolean | 252 | isAAC: boolean |
253 | inputFileMutexReleaser: MutexInterface.Releaser | ||
210 | }) { | 254 | }) { |
211 | return generateHlsPlaylistCommon({ | 255 | return generateHlsPlaylistCommon({ |
212 | video: options.video, | ||
213 | resolution: options.resolution, | ||
214 | inputPath: options.concatenatedTsFilePath, | ||
215 | type: 'hls-from-ts' as 'hls-from-ts', | 256 | type: 'hls-from-ts' as 'hls-from-ts', |
216 | isAAC: options.isAAC | 257 | inputPath: options.concatenatedTsFilePath, |
258 | |||
259 | ...pick(options, [ 'video', 'resolution', 'inputFileMutexReleaser', 'isAAC' ]) | ||
217 | }) | 260 | }) |
218 | } | 261 | } |
219 | 262 | ||
@@ -223,15 +266,14 @@ function generateHlsPlaylistResolution (options: { | |||
223 | videoInputPath: string | 266 | videoInputPath: string |
224 | resolution: VideoResolution | 267 | resolution: VideoResolution |
225 | copyCodecs: boolean | 268 | copyCodecs: boolean |
269 | inputFileMutexReleaser: MutexInterface.Releaser | ||
226 | job?: Job | 270 | job?: Job |
227 | }) { | 271 | }) { |
228 | return generateHlsPlaylistCommon({ | 272 | return generateHlsPlaylistCommon({ |
229 | video: options.video, | ||
230 | resolution: options.resolution, | ||
231 | copyCodecs: options.copyCodecs, | ||
232 | inputPath: options.videoInputPath, | ||
233 | type: 'hls' as 'hls', | 273 | type: 'hls' as 'hls', |
234 | job: options.job | 274 | inputPath: options.videoInputPath, |
275 | |||
276 | ...pick(options, [ 'video', 'resolution', 'copyCodecs', 'inputFileMutexReleaser', 'job' ]) | ||
235 | }) | 277 | }) |
236 | } | 278 | } |
237 | 279 | ||
@@ -251,27 +293,39 @@ async function onWebTorrentVideoFileTranscoding ( | |||
251 | video: MVideoFullLight, | 293 | video: MVideoFullLight, |
252 | videoFile: MVideoFile, | 294 | videoFile: MVideoFile, |
253 | transcodingPath: string, | 295 | transcodingPath: string, |
254 | outputPath: string | 296 | newVideoFile: MVideoFile |
255 | ) { | 297 | ) { |
256 | const stats = await stat(transcodingPath) | 298 | const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
257 | const fps = await getVideoStreamFPS(transcodingPath) | 299 | |
258 | const metadata = await buildFileMetadata(transcodingPath) | 300 | try { |
301 | await video.reload() | ||
302 | |||
303 | const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile) | ||
259 | 304 | ||
260 | await move(transcodingPath, outputPath, { overwrite: true }) | 305 | const stats = await stat(transcodingPath) |
261 | 306 | ||
262 | videoFile.size = stats.size | 307 | const probe = await ffprobePromise(transcodingPath) |
263 | videoFile.fps = fps | 308 | const fps = await getVideoStreamFPS(transcodingPath, probe) |
264 | videoFile.metadata = metadata | 309 | const metadata = await buildFileMetadata(transcodingPath, probe) |
265 | 310 | ||
266 | await createTorrentAndSetInfoHash(video, videoFile) | 311 | await move(transcodingPath, outputPath, { overwrite: true }) |
267 | 312 | ||
268 | const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution }) | 313 | videoFile.size = stats.size |
269 | if (oldFile) await video.removeWebTorrentFile(oldFile) | 314 | videoFile.fps = fps |
315 | videoFile.metadata = metadata | ||
270 | 316 | ||
271 | await VideoFileModel.customUpsert(videoFile, 'video', undefined) | 317 | await createTorrentAndSetInfoHash(video, videoFile) |
272 | video.VideoFiles = await video.$get('VideoFiles') | ||
273 | 318 | ||
274 | return { video, videoFile } | 319 | const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution }) |
320 | if (oldFile) await video.removeWebTorrentFile(oldFile) | ||
321 | |||
322 | await VideoFileModel.customUpsert(videoFile, 'video', undefined) | ||
323 | video.VideoFiles = await video.$get('VideoFiles') | ||
324 | |||
325 | return { video, videoFile } | ||
326 | } finally { | ||
327 | mutexReleaser() | ||
328 | } | ||
275 | } | 329 | } |
276 | 330 | ||
277 | async function generateHlsPlaylistCommon (options: { | 331 | async function generateHlsPlaylistCommon (options: { |
@@ -279,12 +333,15 @@ async function generateHlsPlaylistCommon (options: { | |||
279 | video: MVideo | 333 | video: MVideo |
280 | inputPath: string | 334 | inputPath: string |
281 | resolution: VideoResolution | 335 | resolution: VideoResolution |
336 | |||
337 | inputFileMutexReleaser: MutexInterface.Releaser | ||
338 | |||
282 | copyCodecs?: boolean | 339 | copyCodecs?: boolean |
283 | isAAC?: boolean | 340 | isAAC?: boolean |
284 | 341 | ||
285 | job?: Job | 342 | job?: Job |
286 | }) { | 343 | }) { |
287 | const { type, video, inputPath, resolution, copyCodecs, isAAC, job } = options | 344 | const { type, video, inputPath, resolution, copyCodecs, isAAC, job, inputFileMutexReleaser } = options |
288 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 345 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
289 | 346 | ||
290 | const videoTranscodedBasePath = join(transcodeDirectory, type) | 347 | const videoTranscodedBasePath = join(transcodeDirectory, type) |
@@ -308,6 +365,8 @@ async function generateHlsPlaylistCommon (options: { | |||
308 | 365 | ||
309 | isAAC, | 366 | isAAC, |
310 | 367 | ||
368 | inputFileMutexReleaser, | ||
369 | |||
311 | hlsPlaylist: { | 370 | hlsPlaylist: { |
312 | videoFilename | 371 | videoFilename |
313 | }, | 372 | }, |
@@ -333,40 +392,54 @@ async function generateHlsPlaylistCommon (options: { | |||
333 | videoStreamingPlaylistId: playlist.id | 392 | videoStreamingPlaylistId: playlist.id |
334 | }) | 393 | }) |
335 | 394 | ||
336 | const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile) | 395 | const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
337 | await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video)) | ||
338 | 396 | ||
339 | // Move playlist file | 397 | try { |
340 | const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename) | 398 | // VOD transcoding is a long task, refresh video attributes |
341 | await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true }) | 399 | await video.reload() |
342 | // Move video file | ||
343 | await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true }) | ||
344 | 400 | ||
345 | // Update video duration if it was not set (in case of a live for example) | 401 | const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile) |
346 | if (!video.duration) { | 402 | await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video)) |
347 | video.duration = await getVideoStreamDuration(videoFilePath) | ||
348 | await video.save() | ||
349 | } | ||
350 | 403 | ||
351 | const stats = await stat(videoFilePath) | 404 | // Move playlist file |
405 | const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename) | ||
406 | await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true }) | ||
407 | // Move video file | ||
408 | await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true }) | ||
352 | 409 | ||
353 | newVideoFile.size = stats.size | 410 | // Update video duration if it was not set (in case of a live for example) |
354 | newVideoFile.fps = await getVideoStreamFPS(videoFilePath) | 411 | if (!video.duration) { |
355 | newVideoFile.metadata = await buildFileMetadata(videoFilePath) | 412 | video.duration = await getVideoStreamDuration(videoFilePath) |
413 | await video.save() | ||
414 | } | ||
356 | 415 | ||
357 | await createTorrentAndSetInfoHash(playlist, newVideoFile) | 416 | const stats = await stat(videoFilePath) |
358 | 417 | ||
359 | const oldFile = await VideoFileModel.loadHLSFile({ playlistId: playlist.id, fps: newVideoFile.fps, resolution: newVideoFile.resolution }) | 418 | newVideoFile.size = stats.size |
360 | if (oldFile) { | 419 | newVideoFile.fps = await getVideoStreamFPS(videoFilePath) |
361 | await video.removeStreamingPlaylistVideoFile(playlist, oldFile) | 420 | newVideoFile.metadata = await buildFileMetadata(videoFilePath) |
362 | await oldFile.destroy() | 421 | |
363 | } | 422 | await createTorrentAndSetInfoHash(playlist, newVideoFile) |
364 | 423 | ||
365 | const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) | 424 | const oldFile = await VideoFileModel.loadHLSFile({ |
425 | playlistId: playlist.id, | ||
426 | fps: newVideoFile.fps, | ||
427 | resolution: newVideoFile.resolution | ||
428 | }) | ||
429 | |||
430 | if (oldFile) { | ||
431 | await video.removeStreamingPlaylistVideoFile(playlist, oldFile) | ||
432 | await oldFile.destroy() | ||
433 | } | ||
366 | 434 | ||
367 | await updatePlaylistAfterFileChange(video, playlist) | 435 | const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) |
368 | 436 | ||
369 | return { resolutionPlaylistPath, videoFile: savedVideoFile } | 437 | await updatePlaylistAfterFileChange(video, playlist) |
438 | |||
439 | return { resolutionPlaylistPath, videoFile: savedVideoFile } | ||
440 | } finally { | ||
441 | mutexReleaser() | ||
442 | } | ||
370 | } | 443 | } |
371 | 444 | ||
372 | function buildOriginalFileResolution (inputResolution: number) { | 445 | function buildOriginalFileResolution (inputResolution: number) { |
diff --git a/server/lib/video-path-manager.ts b/server/lib/video-path-manager.ts index c3f55fd95..9953cae5d 100644 --- a/server/lib/video-path-manager.ts +++ b/server/lib/video-path-manager.ts | |||
@@ -1,29 +1,31 @@ | |||
1 | import { Mutex } from 'async-mutex' | ||
1 | import { remove } from 'fs-extra' | 2 | import { remove } from 'fs-extra' |
2 | import { extname, join } from 'path' | 3 | import { extname, join } from 'path' |
4 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
3 | import { extractVideo } from '@server/helpers/video' | 5 | import { extractVideo } from '@server/helpers/video' |
4 | import { CONFIG } from '@server/initializers/config' | 6 | import { CONFIG } from '@server/initializers/config' |
5 | import { | 7 | import { DIRECTORIES } from '@server/initializers/constants' |
6 | MStreamingPlaylistVideo, | 8 | import { MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '@server/types/models' |
7 | MVideo, | ||
8 | MVideoFile, | ||
9 | MVideoFileStreamingPlaylistVideo, | ||
10 | MVideoFileVideo, | ||
11 | MVideoUUID | ||
12 | } from '@server/types/models' | ||
13 | import { buildUUID } from '@shared/extra-utils' | 9 | import { buildUUID } from '@shared/extra-utils' |
14 | import { VideoStorage } from '@shared/models' | 10 | import { VideoStorage } from '@shared/models' |
15 | import { makeHLSFileAvailable, makeWebTorrentFileAvailable } from './object-storage' | 11 | import { makeHLSFileAvailable, makeWebTorrentFileAvailable } from './object-storage' |
16 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from './paths' | 12 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from './paths' |
13 | import { isVideoInPrivateDirectory } from './video-privacy' | ||
17 | 14 | ||
18 | type MakeAvailableCB <T> = (path: string) => Promise<T> | T | 15 | type MakeAvailableCB <T> = (path: string) => Promise<T> | T |
19 | 16 | ||
17 | const lTags = loggerTagsFactory('video-path-manager') | ||
18 | |||
20 | class VideoPathManager { | 19 | class VideoPathManager { |
21 | 20 | ||
22 | private static instance: VideoPathManager | 21 | private static instance: VideoPathManager |
23 | 22 | ||
23 | // Key is a video UUID | ||
24 | private readonly videoFileMutexStore = new Map<string, Mutex>() | ||
25 | |||
24 | private constructor () {} | 26 | private constructor () {} |
25 | 27 | ||
26 | getFSHLSOutputPath (video: MVideoUUID, filename?: string) { | 28 | getFSHLSOutputPath (video: MVideo, filename?: string) { |
27 | const base = getHLSDirectory(video) | 29 | const base = getHLSDirectory(video) |
28 | if (!filename) return base | 30 | if (!filename) return base |
29 | 31 | ||
@@ -41,13 +43,17 @@ class VideoPathManager { | |||
41 | } | 43 | } |
42 | 44 | ||
43 | getFSVideoFileOutputPath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) { | 45 | getFSVideoFileOutputPath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) { |
44 | if (videoFile.isHLS()) { | 46 | const video = extractVideo(videoOrPlaylist) |
45 | const video = extractVideo(videoOrPlaylist) | ||
46 | 47 | ||
48 | if (videoFile.isHLS()) { | ||
47 | return join(getHLSDirectory(video), videoFile.filename) | 49 | return join(getHLSDirectory(video), videoFile.filename) |
48 | } | 50 | } |
49 | 51 | ||
50 | return join(CONFIG.STORAGE.VIDEOS_DIR, videoFile.filename) | 52 | if (isVideoInPrivateDirectory(video.privacy)) { |
53 | return join(DIRECTORIES.VIDEOS.PRIVATE, videoFile.filename) | ||
54 | } | ||
55 | |||
56 | return join(DIRECTORIES.VIDEOS.PUBLIC, videoFile.filename) | ||
51 | } | 57 | } |
52 | 58 | ||
53 | async makeAvailableVideoFile <T> (videoFile: MVideoFileVideo | MVideoFileStreamingPlaylistVideo, cb: MakeAvailableCB<T>) { | 59 | async makeAvailableVideoFile <T> (videoFile: MVideoFileVideo | MVideoFileStreamingPlaylistVideo, cb: MakeAvailableCB<T>) { |
@@ -113,6 +119,27 @@ class VideoPathManager { | |||
113 | ) | 119 | ) |
114 | } | 120 | } |
115 | 121 | ||
122 | async lockFiles (videoUUID: string) { | ||
123 | if (!this.videoFileMutexStore.has(videoUUID)) { | ||
124 | this.videoFileMutexStore.set(videoUUID, new Mutex()) | ||
125 | } | ||
126 | |||
127 | const mutex = this.videoFileMutexStore.get(videoUUID) | ||
128 | const releaser = await mutex.acquire() | ||
129 | |||
130 | logger.debug('Locked files of %s.', videoUUID, lTags(videoUUID)) | ||
131 | |||
132 | return releaser | ||
133 | } | ||
134 | |||
135 | unlockFiles (videoUUID: string) { | ||
136 | const mutex = this.videoFileMutexStore.get(videoUUID) | ||
137 | |||
138 | mutex.release() | ||
139 | |||
140 | logger.debug('Released lockfiles of %s.', videoUUID, lTags(videoUUID)) | ||
141 | } | ||
142 | |||
116 | private async makeAvailableFactory <T> (method: () => Promise<string> | string, clean: boolean, cb: MakeAvailableCB<T>) { | 143 | private async makeAvailableFactory <T> (method: () => Promise<string> | string, clean: boolean, cb: MakeAvailableCB<T>) { |
117 | let result: T | 144 | let result: T |
118 | 145 | ||
diff --git a/server/lib/video-privacy.ts b/server/lib/video-privacy.ts new file mode 100644 index 000000000..1a4a5a22d --- /dev/null +++ b/server/lib/video-privacy.ts | |||
@@ -0,0 +1,96 @@ | |||
1 | import { move } from 'fs-extra' | ||
2 | import { join } from 'path' | ||
3 | import { logger } from '@server/helpers/logger' | ||
4 | import { DIRECTORIES } from '@server/initializers/constants' | ||
5 | import { MVideo, MVideoFullLight } from '@server/types/models' | ||
6 | import { VideoPrivacy } from '@shared/models' | ||
7 | |||
8 | function setVideoPrivacy (video: MVideo, newPrivacy: VideoPrivacy) { | ||
9 | if (video.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) { | ||
10 | video.publishedAt = new Date() | ||
11 | } | ||
12 | |||
13 | video.privacy = newPrivacy | ||
14 | } | ||
15 | |||
16 | function isVideoInPrivateDirectory (privacy: VideoPrivacy) { | ||
17 | return privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.INTERNAL | ||
18 | } | ||
19 | |||
20 | function isVideoInPublicDirectory (privacy: VideoPrivacy) { | ||
21 | return !isVideoInPrivateDirectory(privacy) | ||
22 | } | ||
23 | |||
24 | async function moveFilesIfPrivacyChanged (video: MVideoFullLight, oldPrivacy: VideoPrivacy) { | ||
25 | // Now public, previously private | ||
26 | if (isVideoInPublicDirectory(video.privacy) && isVideoInPrivateDirectory(oldPrivacy)) { | ||
27 | await moveFiles({ type: 'private-to-public', video }) | ||
28 | |||
29 | return true | ||
30 | } | ||
31 | |||
32 | // Now private, previously public | ||
33 | if (isVideoInPrivateDirectory(video.privacy) && isVideoInPublicDirectory(oldPrivacy)) { | ||
34 | await moveFiles({ type: 'public-to-private', video }) | ||
35 | |||
36 | return true | ||
37 | } | ||
38 | |||
39 | return false | ||
40 | } | ||
41 | |||
42 | export { | ||
43 | setVideoPrivacy, | ||
44 | |||
45 | isVideoInPrivateDirectory, | ||
46 | isVideoInPublicDirectory, | ||
47 | |||
48 | moveFilesIfPrivacyChanged | ||
49 | } | ||
50 | |||
51 | // --------------------------------------------------------------------------- | ||
52 | |||
53 | async function moveFiles (options: { | ||
54 | type: 'private-to-public' | 'public-to-private' | ||
55 | video: MVideoFullLight | ||
56 | }) { | ||
57 | const { type, video } = options | ||
58 | |||
59 | const directories = type === 'private-to-public' | ||
60 | ? { | ||
61 | webtorrent: { old: DIRECTORIES.VIDEOS.PRIVATE, new: DIRECTORIES.VIDEOS.PUBLIC }, | ||
62 | hls: { old: DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, new: DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC } | ||
63 | } | ||
64 | : { | ||
65 | webtorrent: { old: DIRECTORIES.VIDEOS.PUBLIC, new: DIRECTORIES.VIDEOS.PRIVATE }, | ||
66 | hls: { old: DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC, new: DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE } | ||
67 | } | ||
68 | |||
69 | for (const file of video.VideoFiles) { | ||
70 | const source = join(directories.webtorrent.old, file.filename) | ||
71 | const destination = join(directories.webtorrent.new, file.filename) | ||
72 | |||
73 | try { | ||
74 | logger.info('Moving WebTorrent files of %s after privacy change (%s -> %s).', video.uuid, source, destination) | ||
75 | |||
76 | await move(source, destination) | ||
77 | } catch (err) { | ||
78 | logger.error('Cannot move webtorrent file %s to %s after privacy change', source, destination, { err }) | ||
79 | } | ||
80 | } | ||
81 | |||
82 | const hls = video.getHLSPlaylist() | ||
83 | |||
84 | if (hls) { | ||
85 | const source = join(directories.hls.old, video.uuid) | ||
86 | const destination = join(directories.hls.new, video.uuid) | ||
87 | |||
88 | try { | ||
89 | logger.info('Moving HLS files of %s after privacy change (%s -> %s).', video.uuid, source, destination) | ||
90 | |||
91 | await move(source, destination) | ||
92 | } catch (err) { | ||
93 | logger.error('Cannot move HLS file %s to %s after privacy change', source, destination, { err }) | ||
94 | } | ||
95 | } | ||
96 | } | ||
diff --git a/server/lib/video-tokens-manager.ts b/server/lib/video-tokens-manager.ts new file mode 100644 index 000000000..c43085d16 --- /dev/null +++ b/server/lib/video-tokens-manager.ts | |||
@@ -0,0 +1,49 @@ | |||
1 | import LRUCache from 'lru-cache' | ||
2 | import { LRU_CACHE } from '@server/initializers/constants' | ||
3 | import { buildUUID } from '@shared/extra-utils' | ||
4 | |||
5 | // --------------------------------------------------------------------------- | ||
6 | // Create temporary tokens that can be used as URL query parameters to access video static files | ||
7 | // --------------------------------------------------------------------------- | ||
8 | |||
9 | class VideoTokensManager { | ||
10 | |||
11 | private static instance: VideoTokensManager | ||
12 | |||
13 | private readonly lruCache = new LRUCache<string, string>({ | ||
14 | max: LRU_CACHE.VIDEO_TOKENS.MAX_SIZE, | ||
15 | ttl: LRU_CACHE.VIDEO_TOKENS.TTL | ||
16 | }) | ||
17 | |||
18 | private constructor () {} | ||
19 | |||
20 | create (videoUUID: string) { | ||
21 | const token = buildUUID() | ||
22 | |||
23 | const expires = new Date(new Date().getTime() + LRU_CACHE.VIDEO_TOKENS.TTL) | ||
24 | |||
25 | this.lruCache.set(token, videoUUID) | ||
26 | |||
27 | return { token, expires } | ||
28 | } | ||
29 | |||
30 | hasToken (options: { | ||
31 | token: string | ||
32 | videoUUID: string | ||
33 | }) { | ||
34 | const value = this.lruCache.get(options.token) | ||
35 | if (!value) return false | ||
36 | |||
37 | return value === options.videoUUID | ||
38 | } | ||
39 | |||
40 | static get Instance () { | ||
41 | return this.instance || (this.instance = new this()) | ||
42 | } | ||
43 | } | ||
44 | |||
45 | // --------------------------------------------------------------------------- | ||
46 | |||
47 | export { | ||
48 | VideoTokensManager | ||
49 | } | ||
diff --git a/server/lib/video.ts b/server/lib/video.ts index 6c4f3ce7b..aacc41a7a 100644 --- a/server/lib/video.ts +++ b/server/lib/video.ts | |||
@@ -7,10 +7,11 @@ import { TagModel } from '@server/models/video/tag' | |||
7 | import { VideoModel } from '@server/models/video/video' | 7 | import { VideoModel } from '@server/models/video/video' |
8 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' | 8 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' |
9 | import { FilteredModelAttributes } from '@server/types' | 9 | import { FilteredModelAttributes } from '@server/types' |
10 | import { MThumbnail, MUserId, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' | 10 | import { MThumbnail, MUserId, MVideoFile, MVideoFullLight, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' |
11 | import { ThumbnailType, VideoCreate, VideoPrivacy, VideoState, VideoTranscodingPayload } from '@shared/models' | 11 | import { ManageVideoTorrentPayload, ThumbnailType, VideoCreate, VideoPrivacy, VideoState, VideoTranscodingPayload } from '@shared/models' |
12 | import { CreateJobOptions } from './job-queue/job-queue' | 12 | import { CreateJobArgument, CreateJobOptions, JobQueue } from './job-queue/job-queue' |
13 | import { updateVideoMiniatureFromExisting } from './thumbnail' | 13 | import { updateVideoMiniatureFromExisting } from './thumbnail' |
14 | import { moveFilesIfPrivacyChanged } from './video-privacy' | ||
14 | 15 | ||
15 | function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> { | 16 | function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> { |
16 | return { | 17 | return { |
@@ -177,6 +178,59 @@ const getCachedVideoDuration = memoizee(getVideoDuration, { | |||
177 | 178 | ||
178 | // --------------------------------------------------------------------------- | 179 | // --------------------------------------------------------------------------- |
179 | 180 | ||
181 | async function addVideoJobsAfterUpdate (options: { | ||
182 | video: MVideoFullLight | ||
183 | isNewVideo: boolean | ||
184 | |||
185 | nameChanged: boolean | ||
186 | oldPrivacy: VideoPrivacy | ||
187 | }) { | ||
188 | const { video, nameChanged, oldPrivacy, isNewVideo } = options | ||
189 | const jobs: CreateJobArgument[] = [] | ||
190 | |||
191 | const filePathChanged = await moveFilesIfPrivacyChanged(video, oldPrivacy) | ||
192 | |||
193 | if (!video.isLive && (nameChanged || filePathChanged)) { | ||
194 | for (const file of (video.VideoFiles || [])) { | ||
195 | const payload: ManageVideoTorrentPayload = { action: 'update-metadata', videoId: video.id, videoFileId: file.id } | ||
196 | |||
197 | jobs.push({ type: 'manage-video-torrent', payload }) | ||
198 | } | ||
199 | |||
200 | const hls = video.getHLSPlaylist() | ||
201 | |||
202 | for (const file of (hls?.VideoFiles || [])) { | ||
203 | const payload: ManageVideoTorrentPayload = { action: 'update-metadata', streamingPlaylistId: hls.id, videoFileId: file.id } | ||
204 | |||
205 | jobs.push({ type: 'manage-video-torrent', payload }) | ||
206 | } | ||
207 | } | ||
208 | |||
209 | jobs.push({ | ||
210 | type: 'federate-video', | ||
211 | payload: { | ||
212 | videoUUID: video.uuid, | ||
213 | isNewVideo | ||
214 | } | ||
215 | }) | ||
216 | |||
217 | const wasConfidentialVideo = new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.INTERNAL ]).has(oldPrivacy) | ||
218 | |||
219 | if (wasConfidentialVideo) { | ||
220 | jobs.push({ | ||
221 | type: 'notify', | ||
222 | payload: { | ||
223 | action: 'new-video', | ||
224 | videoUUID: video.uuid | ||
225 | } | ||
226 | }) | ||
227 | } | ||
228 | |||
229 | return JobQueue.Instance.createSequentialJobFlow(...jobs) | ||
230 | } | ||
231 | |||
232 | // --------------------------------------------------------------------------- | ||
233 | |||
180 | export { | 234 | export { |
181 | buildLocalVideoFromReq, | 235 | buildLocalVideoFromReq, |
182 | buildVideoThumbnailsFromReq, | 236 | buildVideoThumbnailsFromReq, |
@@ -185,5 +239,6 @@ export { | |||
185 | buildTranscodingJob, | 239 | buildTranscodingJob, |
186 | buildMoveToObjectStorageJob, | 240 | buildMoveToObjectStorageJob, |
187 | getTranscodingJobPriority, | 241 | getTranscodingJobPriority, |
242 | addVideoJobsAfterUpdate, | ||
188 | getCachedVideoDuration | 243 | getCachedVideoDuration |
189 | } | 244 | } |
diff --git a/server/middlewares/auth.ts b/server/middlewares/auth.ts index 904d47efd..e6025c8ce 100644 --- a/server/middlewares/auth.ts +++ b/server/middlewares/auth.ts | |||
@@ -5,8 +5,8 @@ import { HttpStatusCode } from '../../shared/models/http/http-error-codes' | |||
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
6 | import { handleOAuthAuthenticate } from '../lib/auth/oauth' | 6 | import { handleOAuthAuthenticate } from '../lib/auth/oauth' |
7 | 7 | ||
8 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) { | 8 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) { |
9 | handleOAuthAuthenticate(req, res, authenticateInQuery) | 9 | handleOAuthAuthenticate(req, res) |
10 | .then((token: any) => { | 10 | .then((token: any) => { |
11 | res.locals.oauth = { token } | 11 | res.locals.oauth = { token } |
12 | res.locals.authenticated = true | 12 | res.locals.authenticated = true |
@@ -47,7 +47,7 @@ function authenticateSocket (socket: Socket, next: (err?: any) => void) { | |||
47 | .catch(err => logger.error('Cannot get access token.', { err })) | 47 | .catch(err => logger.error('Cannot get access token.', { err })) |
48 | } | 48 | } |
49 | 49 | ||
50 | function authenticatePromise (req: express.Request, res: express.Response, authenticateInQuery = false) { | 50 | function authenticatePromise (req: express.Request, res: express.Response) { |
51 | return new Promise<void>(resolve => { | 51 | return new Promise<void>(resolve => { |
52 | // Already authenticated? (or tried to) | 52 | // Already authenticated? (or tried to) |
53 | if (res.locals.oauth?.token.User) return resolve() | 53 | if (res.locals.oauth?.token.User) return resolve() |
@@ -59,7 +59,7 @@ function authenticatePromise (req: express.Request, res: express.Response, authe | |||
59 | }) | 59 | }) |
60 | } | 60 | } |
61 | 61 | ||
62 | authenticate(req, res, () => resolve(), authenticateInQuery) | 62 | authenticate(req, res, () => resolve()) |
63 | }) | 63 | }) |
64 | } | 64 | } |
65 | 65 | ||
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts index ffadb3b49..899da229a 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | export * from './activitypub' | ||
2 | export * from './videos' | ||
3 | export * from './abuse' | 1 | export * from './abuse' |
4 | export * from './account' | 2 | export * from './account' |
3 | export * from './activitypub' | ||
5 | export * from './actor-image' | 4 | export * from './actor-image' |
6 | export * from './blocklist' | 5 | export * from './blocklist' |
7 | export * from './bulk' | 6 | export * from './bulk' |
@@ -10,8 +9,8 @@ export * from './express' | |||
10 | export * from './feeds' | 9 | export * from './feeds' |
11 | export * from './follows' | 10 | export * from './follows' |
12 | export * from './jobs' | 11 | export * from './jobs' |
13 | export * from './metrics' | ||
14 | export * from './logs' | 12 | export * from './logs' |
13 | export * from './metrics' | ||
15 | export * from './oembed' | 14 | export * from './oembed' |
16 | export * from './pagination' | 15 | export * from './pagination' |
17 | export * from './plugins' | 16 | export * from './plugins' |
@@ -19,9 +18,11 @@ export * from './redundancy' | |||
19 | export * from './search' | 18 | export * from './search' |
20 | export * from './server' | 19 | export * from './server' |
21 | export * from './sort' | 20 | export * from './sort' |
21 | export * from './static' | ||
22 | export * from './themes' | 22 | export * from './themes' |
23 | export * from './user-history' | 23 | export * from './user-history' |
24 | export * from './user-notifications' | 24 | export * from './user-notifications' |
25 | export * from './user-subscriptions' | 25 | export * from './user-subscriptions' |
26 | export * from './users' | 26 | export * from './users' |
27 | export * from './videos' | ||
27 | export * from './webfinger' | 28 | export * from './webfinger' |
diff --git a/server/middlewares/validators/shared/videos.ts b/server/middlewares/validators/shared/videos.ts index e3a98c58f..c29751eca 100644 --- a/server/middlewares/validators/shared/videos.ts +++ b/server/middlewares/validators/shared/videos.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Request, Response } from 'express' | 1 | import { Request, Response } from 'express' |
2 | import { isUUIDValid } from '@server/helpers/custom-validators/misc' | ||
3 | import { loadVideo, VideoLoadType } from '@server/lib/model-loaders' | 2 | import { loadVideo, VideoLoadType } from '@server/lib/model-loaders' |
4 | import { isAbleToUploadVideo } from '@server/lib/user' | 3 | import { isAbleToUploadVideo } from '@server/lib/user' |
4 | import { VideoTokensManager } from '@server/lib/video-tokens-manager' | ||
5 | import { authenticatePromise } from '@server/middlewares/auth' | 5 | import { authenticatePromise } from '@server/middlewares/auth' |
6 | import { VideoModel } from '@server/models/video/video' | 6 | import { VideoModel } from '@server/models/video/video' |
7 | import { VideoChannelModel } from '@server/models/video/video-channel' | 7 | import { VideoChannelModel } from '@server/models/video/video-channel' |
@@ -108,26 +108,21 @@ async function checkCanSeeVideo (options: { | |||
108 | res: Response | 108 | res: Response |
109 | paramId: string | 109 | paramId: string |
110 | video: MVideo | 110 | video: MVideo |
111 | authenticateInQuery?: boolean // default false | ||
112 | }) { | 111 | }) { |
113 | const { req, res, video, paramId, authenticateInQuery = false } = options | 112 | const { req, res, video, paramId } = options |
114 | 113 | ||
115 | if (video.requiresAuth()) { | 114 | if (video.requiresAuth(paramId)) { |
116 | return checkCanSeeAuthVideo(req, res, video, authenticateInQuery) | 115 | return checkCanSeeAuthVideo(req, res, video) |
117 | } | 116 | } |
118 | 117 | ||
119 | if (video.privacy === VideoPrivacy.UNLISTED) { | 118 | if (video.privacy === VideoPrivacy.UNLISTED || video.privacy === VideoPrivacy.PUBLIC) { |
120 | if (isUUIDValid(paramId)) return true | 119 | return true |
121 | |||
122 | return checkCanSeeAuthVideo(req, res, video, authenticateInQuery) | ||
123 | } | 120 | } |
124 | 121 | ||
125 | if (video.privacy === VideoPrivacy.PUBLIC) return true | 122 | throw new Error('Unknown video privacy when checking video right ' + video.url) |
126 | |||
127 | throw new Error('Fatal error when checking video right ' + video.url) | ||
128 | } | 123 | } |
129 | 124 | ||
130 | async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoId | MVideoWithRights, authenticateInQuery = false) { | 125 | async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoId | MVideoWithRights) { |
131 | const fail = () => { | 126 | const fail = () => { |
132 | res.fail({ | 127 | res.fail({ |
133 | status: HttpStatusCode.FORBIDDEN_403, | 128 | status: HttpStatusCode.FORBIDDEN_403, |
@@ -137,7 +132,7 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI | |||
137 | return false | 132 | return false |
138 | } | 133 | } |
139 | 134 | ||
140 | await authenticatePromise(req, res, authenticateInQuery) | 135 | await authenticatePromise(req, res) |
141 | 136 | ||
142 | const user = res.locals.oauth?.token.User | 137 | const user = res.locals.oauth?.token.User |
143 | if (!user) return fail() | 138 | if (!user) return fail() |
@@ -173,6 +168,36 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI | |||
173 | 168 | ||
174 | // --------------------------------------------------------------------------- | 169 | // --------------------------------------------------------------------------- |
175 | 170 | ||
171 | async function checkCanAccessVideoStaticFiles (options: { | ||
172 | video: MVideo | ||
173 | req: Request | ||
174 | res: Response | ||
175 | paramId: string | ||
176 | }) { | ||
177 | const { video, req, res, paramId } = options | ||
178 | |||
179 | if (res.locals.oauth?.token.User) { | ||
180 | return checkCanSeeVideo(options) | ||
181 | } | ||
182 | |||
183 | if (!video.requiresAuth(paramId)) return true | ||
184 | |||
185 | const videoFileToken = req.query.videoFileToken | ||
186 | if (!videoFileToken) { | ||
187 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | ||
188 | return false | ||
189 | } | ||
190 | |||
191 | if (VideoTokensManager.Instance.hasToken({ token: videoFileToken, videoUUID: video.uuid })) { | ||
192 | return true | ||
193 | } | ||
194 | |||
195 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | ||
196 | return false | ||
197 | } | ||
198 | |||
199 | // --------------------------------------------------------------------------- | ||
200 | |||
176 | function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) { | 201 | function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) { |
177 | // Retrieve the user who did the request | 202 | // Retrieve the user who did the request |
178 | if (onlyOwned && video.isOwned() === false) { | 203 | if (onlyOwned && video.isOwned() === false) { |
@@ -220,6 +245,7 @@ export { | |||
220 | doesVideoExist, | 245 | doesVideoExist, |
221 | doesVideoFileOfVideoExist, | 246 | doesVideoFileOfVideoExist, |
222 | 247 | ||
248 | checkCanAccessVideoStaticFiles, | ||
223 | checkUserCanManageVideo, | 249 | checkUserCanManageVideo, |
224 | checkCanSeeVideo, | 250 | checkCanSeeVideo, |
225 | checkUserQuota | 251 | checkUserQuota |
diff --git a/server/middlewares/validators/static.ts b/server/middlewares/validators/static.ts new file mode 100644 index 000000000..ff9e6ae6e --- /dev/null +++ b/server/middlewares/validators/static.ts | |||
@@ -0,0 +1,131 @@ | |||
1 | import express from 'express' | ||
2 | import { query } from 'express-validator' | ||
3 | import LRUCache from 'lru-cache' | ||
4 | import { basename, dirname } from 'path' | ||
5 | import { exists, isUUIDValid } from '@server/helpers/custom-validators/misc' | ||
6 | import { logger } from '@server/helpers/logger' | ||
7 | import { LRU_CACHE } from '@server/initializers/constants' | ||
8 | import { VideoModel } from '@server/models/video/video' | ||
9 | import { VideoFileModel } from '@server/models/video/video-file' | ||
10 | import { HttpStatusCode } from '@shared/models' | ||
11 | import { areValidationErrors, checkCanAccessVideoStaticFiles } from './shared' | ||
12 | |||
13 | const staticFileTokenBypass = new LRUCache<string, boolean>({ | ||
14 | max: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.MAX_SIZE, | ||
15 | ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL | ||
16 | }) | ||
17 | |||
18 | const ensureCanAccessVideoPrivateWebTorrentFiles = [ | ||
19 | query('videoFileToken').optional().custom(exists), | ||
20 | |||
21 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
22 | if (areValidationErrors(req, res)) return | ||
23 | |||
24 | const token = extractTokenOrDie(req, res) | ||
25 | if (!token) return | ||
26 | |||
27 | const cacheKey = token + '-' + req.originalUrl | ||
28 | |||
29 | if (staticFileTokenBypass.has(cacheKey)) { | ||
30 | const allowedFromCache = staticFileTokenBypass.get(cacheKey) | ||
31 | |||
32 | if (allowedFromCache === true) return next() | ||
33 | |||
34 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | ||
35 | } | ||
36 | |||
37 | const allowed = await isWebTorrentAllowed(req, res) | ||
38 | |||
39 | staticFileTokenBypass.set(cacheKey, allowed) | ||
40 | |||
41 | if (allowed !== true) return | ||
42 | |||
43 | return next() | ||
44 | } | ||
45 | ] | ||
46 | |||
47 | const ensureCanAccessPrivateVideoHLSFiles = [ | ||
48 | query('videoFileToken').optional().custom(exists), | ||
49 | |||
50 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
51 | if (areValidationErrors(req, res)) return | ||
52 | |||
53 | const videoUUID = basename(dirname(req.originalUrl)) | ||
54 | |||
55 | if (!isUUIDValid(videoUUID)) { | ||
56 | logger.debug('Path does not contain valid video UUID to serve static file %s', req.originalUrl) | ||
57 | |||
58 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | ||
59 | } | ||
60 | |||
61 | const token = extractTokenOrDie(req, res) | ||
62 | if (!token) return | ||
63 | |||
64 | const cacheKey = token + '-' + videoUUID | ||
65 | |||
66 | if (staticFileTokenBypass.has(cacheKey)) { | ||
67 | const allowedFromCache = staticFileTokenBypass.get(cacheKey) | ||
68 | |||
69 | if (allowedFromCache === true) return next() | ||
70 | |||
71 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | ||
72 | } | ||
73 | |||
74 | const allowed = await isHLSAllowed(req, res, videoUUID) | ||
75 | |||
76 | staticFileTokenBypass.set(cacheKey, allowed) | ||
77 | |||
78 | if (allowed !== true) return | ||
79 | |||
80 | return next() | ||
81 | } | ||
82 | ] | ||
83 | |||
84 | export { | ||
85 | ensureCanAccessVideoPrivateWebTorrentFiles, | ||
86 | ensureCanAccessPrivateVideoHLSFiles | ||
87 | } | ||
88 | |||
89 | // --------------------------------------------------------------------------- | ||
90 | |||
91 | async function isWebTorrentAllowed (req: express.Request, res: express.Response) { | ||
92 | const filename = basename(req.path) | ||
93 | |||
94 | const file = await VideoFileModel.loadWithVideoByFilename(filename) | ||
95 | if (!file) { | ||
96 | logger.debug('Unknown static file %s to serve', req.originalUrl, { filename }) | ||
97 | |||
98 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | ||
99 | return false | ||
100 | } | ||
101 | |||
102 | const video = file.getVideo() | ||
103 | |||
104 | return checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid }) | ||
105 | } | ||
106 | |||
107 | async function isHLSAllowed (req: express.Request, res: express.Response, videoUUID: string) { | ||
108 | const video = await VideoModel.load(videoUUID) | ||
109 | |||
110 | if (!video) { | ||
111 | logger.debug('Unknown static file %s to serve', req.originalUrl, { videoUUID }) | ||
112 | |||
113 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | ||
114 | return false | ||
115 | } | ||
116 | |||
117 | return checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid }) | ||
118 | } | ||
119 | |||
120 | function extractTokenOrDie (req: express.Request, res: express.Response) { | ||
121 | const token = res.locals.oauth?.token.accessToken || req.query.videoFileToken | ||
122 | |||
123 | if (!token) { | ||
124 | return res.fail({ | ||
125 | message: 'Bearer token is missing in headers or video file token is missing in URL query parameters', | ||
126 | status: HttpStatusCode.FORBIDDEN_403 | ||
127 | }) | ||
128 | } | ||
129 | |||
130 | return token | ||
131 | } | ||
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 7fd2b03d1..e29eb4a32 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -7,7 +7,7 @@ import { getServerActor } from '@server/models/application/application' | |||
7 | import { ExpressPromiseHandler } from '@server/types/express-handler' | 7 | import { ExpressPromiseHandler } from '@server/types/express-handler' |
8 | import { MUserAccountId, MVideoFullLight } from '@server/types/models' | 8 | import { MUserAccountId, MVideoFullLight } from '@server/types/models' |
9 | import { arrayify, getAllPrivacies } from '@shared/core-utils' | 9 | import { arrayify, getAllPrivacies } from '@shared/core-utils' |
10 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude } from '@shared/models' | 10 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude, VideoState } from '@shared/models' |
11 | import { | 11 | import { |
12 | exists, | 12 | exists, |
13 | isBooleanValid, | 13 | isBooleanValid, |
@@ -48,6 +48,7 @@ import { Hooks } from '../../../lib/plugins/hooks' | |||
48 | import { VideoModel } from '../../../models/video/video' | 48 | import { VideoModel } from '../../../models/video/video' |
49 | import { | 49 | import { |
50 | areValidationErrors, | 50 | areValidationErrors, |
51 | checkCanAccessVideoStaticFiles, | ||
51 | checkCanSeeVideo, | 52 | checkCanSeeVideo, |
52 | checkUserCanManageVideo, | 53 | checkUserCanManageVideo, |
53 | checkUserQuota, | 54 | checkUserQuota, |
@@ -232,6 +233,11 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([ | |||
232 | if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) | 233 | if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) |
233 | if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req) | 234 | if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req) |
234 | 235 | ||
236 | const video = getVideoWithAttributes(res) | ||
237 | if (req.body.privacy && video.isLive && video.state !== VideoState.WAITING_FOR_LIVE) { | ||
238 | return res.fail({ message: 'Cannot update privacy of a live that has already started' }) | ||
239 | } | ||
240 | |||
235 | // Check if the user who did the request is able to update the video | 241 | // Check if the user who did the request is able to update the video |
236 | const user = res.locals.oauth.token.User | 242 | const user = res.locals.oauth.token.User |
237 | if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req) | 243 | if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req) |
@@ -271,10 +277,7 @@ async function checkVideoFollowConstraints (req: express.Request, res: express.R | |||
271 | }) | 277 | }) |
272 | } | 278 | } |
273 | 279 | ||
274 | const videosCustomGetValidator = ( | 280 | const videosCustomGetValidator = (fetchType: 'for-api' | 'all' | 'only-video' | 'only-immutable-attributes') => { |
275 | fetchType: 'for-api' | 'all' | 'only-video' | 'only-immutable-attributes', | ||
276 | authenticateInQuery = false | ||
277 | ) => { | ||
278 | return [ | 281 | return [ |
279 | isValidVideoIdParam('id'), | 282 | isValidVideoIdParam('id'), |
280 | 283 | ||
@@ -287,7 +290,7 @@ const videosCustomGetValidator = ( | |||
287 | 290 | ||
288 | const video = getVideoWithAttributes(res) as MVideoFullLight | 291 | const video = getVideoWithAttributes(res) as MVideoFullLight |
289 | 292 | ||
290 | if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.id, authenticateInQuery })) return | 293 | if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.id })) return |
291 | 294 | ||
292 | return next() | 295 | return next() |
293 | } | 296 | } |
@@ -295,7 +298,6 @@ const videosCustomGetValidator = ( | |||
295 | } | 298 | } |
296 | 299 | ||
297 | const videosGetValidator = videosCustomGetValidator('all') | 300 | const videosGetValidator = videosCustomGetValidator('all') |
298 | const videosDownloadValidator = videosCustomGetValidator('all', true) | ||
299 | 301 | ||
300 | const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([ | 302 | const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([ |
301 | isValidVideoIdParam('id'), | 303 | isValidVideoIdParam('id'), |
@@ -311,6 +313,21 @@ const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([ | |||
311 | } | 313 | } |
312 | ]) | 314 | ]) |
313 | 315 | ||
316 | const videosDownloadValidator = [ | ||
317 | isValidVideoIdParam('id'), | ||
318 | |||
319 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
320 | if (areValidationErrors(req, res)) return | ||
321 | if (!await doesVideoExist(req.params.id, res, 'all')) return | ||
322 | |||
323 | const video = getVideoWithAttributes(res) | ||
324 | |||
325 | if (!await checkCanAccessVideoStaticFiles({ req, res, video, paramId: req.params.id })) return | ||
326 | |||
327 | return next() | ||
328 | } | ||
329 | ] | ||
330 | |||
314 | const videosRemoveValidator = [ | 331 | const videosRemoveValidator = [ |
315 | isValidVideoIdParam('id'), | 332 | isValidVideoIdParam('id'), |
316 | 333 | ||
@@ -372,7 +389,7 @@ function getCommonVideoEditAttributes () { | |||
372 | .custom(isBooleanValid).withMessage('Should have a valid waitTranscoding boolean'), | 389 | .custom(isBooleanValid).withMessage('Should have a valid waitTranscoding boolean'), |
373 | body('privacy') | 390 | body('privacy') |
374 | .optional() | 391 | .optional() |
375 | .customSanitizer(toValueOrNull) | 392 | .customSanitizer(toIntOrNull) |
376 | .custom(isVideoPrivacyValid), | 393 | .custom(isVideoPrivacyValid), |
377 | body('description') | 394 | body('description') |
378 | .optional() | 395 | .optional() |
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts index e1b0eb610..76745f4b5 100644 --- a/server/models/video/formatter/video-format-utils.ts +++ b/server/models/video/formatter/video-format-utils.ts | |||
@@ -34,6 +34,7 @@ import { | |||
34 | import { | 34 | import { |
35 | MServer, | 35 | MServer, |
36 | MStreamingPlaylistRedundanciesOpt, | 36 | MStreamingPlaylistRedundanciesOpt, |
37 | MUserId, | ||
37 | MVideo, | 38 | MVideo, |
38 | MVideoAP, | 39 | MVideoAP, |
39 | MVideoFile, | 40 | MVideoFile, |
@@ -245,8 +246,12 @@ function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) { | |||
245 | function videoFilesModelToFormattedJSON ( | 246 | function videoFilesModelToFormattedJSON ( |
246 | video: MVideoFormattable, | 247 | video: MVideoFormattable, |
247 | videoFiles: MVideoFileRedundanciesOpt[], | 248 | videoFiles: MVideoFileRedundanciesOpt[], |
248 | includeMagnet = true | 249 | options: { |
250 | includeMagnet?: boolean // default true | ||
251 | } = {} | ||
249 | ): VideoFile[] { | 252 | ): VideoFile[] { |
253 | const { includeMagnet = true } = options | ||
254 | |||
250 | const trackerUrls = includeMagnet | 255 | const trackerUrls = includeMagnet |
251 | ? video.getTrackerUrls() | 256 | ? video.getTrackerUrls() |
252 | : [] | 257 | : [] |
@@ -281,11 +286,14 @@ function videoFilesModelToFormattedJSON ( | |||
281 | }) | 286 | }) |
282 | } | 287 | } |
283 | 288 | ||
284 | function addVideoFilesInAPAcc ( | 289 | function addVideoFilesInAPAcc (options: { |
285 | acc: ActivityUrlObject[] | ActivityTagObject[], | 290 | acc: ActivityUrlObject[] | ActivityTagObject[] |
286 | video: MVideo, | 291 | video: MVideo |
287 | files: MVideoFile[] | 292 | files: MVideoFile[] |
288 | ) { | 293 | user?: MUserId |
294 | }) { | ||
295 | const { acc, video, files } = options | ||
296 | |||
289 | const trackerUrls = video.getTrackerUrls() | 297 | const trackerUrls = video.getTrackerUrls() |
290 | 298 | ||
291 | const sortedFiles = (files || []) | 299 | const sortedFiles = (files || []) |
@@ -370,7 +378,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
370 | } | 378 | } |
371 | ] | 379 | ] |
372 | 380 | ||
373 | addVideoFilesInAPAcc(url, video, video.VideoFiles || []) | 381 | addVideoFilesInAPAcc({ acc: url, video, files: video.VideoFiles || [] }) |
374 | 382 | ||
375 | for (const playlist of (video.VideoStreamingPlaylists || [])) { | 383 | for (const playlist of (video.VideoStreamingPlaylists || [])) { |
376 | const tag = playlist.p2pMediaLoaderInfohashes | 384 | const tag = playlist.p2pMediaLoaderInfohashes |
@@ -382,7 +390,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
382 | href: playlist.getSha256SegmentsUrl(video) | 390 | href: playlist.getSha256SegmentsUrl(video) |
383 | }) | 391 | }) |
384 | 392 | ||
385 | addVideoFilesInAPAcc(tag, video, playlist.VideoFiles || []) | 393 | addVideoFilesInAPAcc({ acc: tag, video, files: playlist.VideoFiles || [] }) |
386 | 394 | ||
387 | url.push({ | 395 | url.push({ |
388 | type: 'Link', | 396 | type: 'Link', |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index d4f07f85f..1a608932f 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -24,6 +24,7 @@ import { extractVideo } from '@server/helpers/video' | |||
24 | import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url' | 24 | import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url' |
25 | import { getHLSPublicFileUrl, getWebTorrentPublicFileUrl } from '@server/lib/object-storage' | 25 | import { getHLSPublicFileUrl, getWebTorrentPublicFileUrl } from '@server/lib/object-storage' |
26 | import { getFSTorrentFilePath } from '@server/lib/paths' | 26 | import { getFSTorrentFilePath } from '@server/lib/paths' |
27 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' | ||
27 | import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models' | 28 | import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models' |
28 | import { VideoResolution, VideoStorage } from '@shared/models' | 29 | import { VideoResolution, VideoStorage } from '@shared/models' |
29 | import { AttributesOnly } from '@shared/typescript-utils' | 30 | import { AttributesOnly } from '@shared/typescript-utils' |
@@ -295,6 +296,16 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
295 | return VideoFileModel.findOne(query) | 296 | return VideoFileModel.findOne(query) |
296 | } | 297 | } |
297 | 298 | ||
299 | static loadWithVideoByFilename (filename: string): Promise<MVideoFileVideo | MVideoFileStreamingPlaylistVideo> { | ||
300 | const query = { | ||
301 | where: { | ||
302 | filename | ||
303 | } | ||
304 | } | ||
305 | |||
306 | return VideoFileModel.scope(ScopeNames.WITH_VIDEO_OR_PLAYLIST).findOne(query) | ||
307 | } | ||
308 | |||
298 | static loadWithVideoOrPlaylistByTorrentFilename (filename: string) { | 309 | static loadWithVideoOrPlaylistByTorrentFilename (filename: string) { |
299 | const query = { | 310 | const query = { |
300 | where: { | 311 | where: { |
@@ -305,6 +316,10 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
305 | return VideoFileModel.scope(ScopeNames.WITH_VIDEO_OR_PLAYLIST).findOne(query) | 316 | return VideoFileModel.scope(ScopeNames.WITH_VIDEO_OR_PLAYLIST).findOne(query) |
306 | } | 317 | } |
307 | 318 | ||
319 | static load (id: number): Promise<MVideoFile> { | ||
320 | return VideoFileModel.findByPk(id) | ||
321 | } | ||
322 | |||
308 | static loadWithMetadata (id: number) { | 323 | static loadWithMetadata (id: number) { |
309 | return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id) | 324 | return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id) |
310 | } | 325 | } |
@@ -467,7 +482,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
467 | } | 482 | } |
468 | 483 | ||
469 | getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo { | 484 | getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo { |
470 | if (this.videoId) return (this as MVideoFileVideo).Video | 485 | if (this.videoId || (this as MVideoFileVideo).Video) return (this as MVideoFileVideo).Video |
471 | 486 | ||
472 | return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist | 487 | return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist |
473 | } | 488 | } |
@@ -508,7 +523,17 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
508 | } | 523 | } |
509 | 524 | ||
510 | getFileStaticPath (video: MVideo) { | 525 | getFileStaticPath (video: MVideo) { |
511 | if (this.isHLS()) return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.filename) | 526 | if (this.isHLS()) { |
527 | if (isVideoInPrivateDirectory(video.privacy)) { | ||
528 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, video.uuid, this.filename) | ||
529 | } | ||
530 | |||
531 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.filename) | ||
532 | } | ||
533 | |||
534 | if (isVideoInPrivateDirectory(video.privacy)) { | ||
535 | return join(STATIC_PATHS.PRIVATE_WEBSEED, this.filename) | ||
536 | } | ||
512 | 537 | ||
513 | return join(STATIC_PATHS.WEBSEED, this.filename) | 538 | return join(STATIC_PATHS.WEBSEED, this.filename) |
514 | } | 539 | } |
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts index 2b6771f27..b919046ed 100644 --- a/server/models/video/video-streaming-playlist.ts +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -17,6 +17,7 @@ import { | |||
17 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
18 | import { getHLSPublicFileUrl } from '@server/lib/object-storage' | 18 | import { getHLSPublicFileUrl } from '@server/lib/object-storage' |
19 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '@server/lib/paths' | 19 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '@server/lib/paths' |
20 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' | ||
20 | import { VideoFileModel } from '@server/models/video/video-file' | 21 | import { VideoFileModel } from '@server/models/video/video-file' |
21 | import { MStreamingPlaylist, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models' | 22 | import { MStreamingPlaylist, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models' |
22 | import { sha1 } from '@shared/extra-utils' | 23 | import { sha1 } from '@shared/extra-utils' |
@@ -250,7 +251,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
250 | return getHLSPublicFileUrl(this.playlistUrl) | 251 | return getHLSPublicFileUrl(this.playlistUrl) |
251 | } | 252 | } |
252 | 253 | ||
253 | return WEBSERVER.URL + this.getMasterPlaylistStaticPath(video.uuid) | 254 | return WEBSERVER.URL + this.getMasterPlaylistStaticPath(video) |
254 | } | 255 | } |
255 | 256 | ||
256 | return this.playlistUrl | 257 | return this.playlistUrl |
@@ -262,7 +263,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
262 | return getHLSPublicFileUrl(this.segmentsSha256Url) | 263 | return getHLSPublicFileUrl(this.segmentsSha256Url) |
263 | } | 264 | } |
264 | 265 | ||
265 | return WEBSERVER.URL + this.getSha256SegmentsStaticPath(video.uuid) | 266 | return WEBSERVER.URL + this.getSha256SegmentsStaticPath(video) |
266 | } | 267 | } |
267 | 268 | ||
268 | return this.segmentsSha256Url | 269 | return this.segmentsSha256Url |
@@ -287,11 +288,19 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
287 | return Object.assign(this, { Video: video }) | 288 | return Object.assign(this, { Video: video }) |
288 | } | 289 | } |
289 | 290 | ||
290 | private getMasterPlaylistStaticPath (videoUUID: string) { | 291 | private getMasterPlaylistStaticPath (video: MVideo) { |
291 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.playlistFilename) | 292 | if (isVideoInPrivateDirectory(video.privacy)) { |
293 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, video.uuid, this.playlistFilename) | ||
294 | } | ||
295 | |||
296 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.playlistFilename) | ||
292 | } | 297 | } |
293 | 298 | ||
294 | private getSha256SegmentsStaticPath (videoUUID: string) { | 299 | private getSha256SegmentsStaticPath (video: MVideo) { |
295 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.segmentsSha256Filename) | 300 | if (isVideoInPrivateDirectory(video.privacy)) { |
301 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, video.uuid, this.segmentsSha256Filename) | ||
302 | } | ||
303 | |||
304 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.segmentsSha256Filename) | ||
296 | } | 305 | } |
297 | } | 306 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 468117504..82362917e 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -52,7 +52,7 @@ import { | |||
52 | import { AttributesOnly } from '@shared/typescript-utils' | 52 | import { AttributesOnly } from '@shared/typescript-utils' |
53 | import { peertubeTruncate } from '../../helpers/core-utils' | 53 | import { peertubeTruncate } from '../../helpers/core-utils' |
54 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 54 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
55 | import { exists, isBooleanValid } from '../../helpers/custom-validators/misc' | 55 | import { exists, isBooleanValid, isUUIDValid } from '../../helpers/custom-validators/misc' |
56 | import { | 56 | import { |
57 | isVideoDescriptionValid, | 57 | isVideoDescriptionValid, |
58 | isVideoDurationValid, | 58 | isVideoDurationValid, |
@@ -1696,12 +1696,12 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1696 | let files: VideoFile[] = [] | 1696 | let files: VideoFile[] = [] |
1697 | 1697 | ||
1698 | if (Array.isArray(this.VideoFiles)) { | 1698 | if (Array.isArray(this.VideoFiles)) { |
1699 | const result = videoFilesModelToFormattedJSON(this, this.VideoFiles, includeMagnet) | 1699 | const result = videoFilesModelToFormattedJSON(this, this.VideoFiles, { includeMagnet }) |
1700 | files = files.concat(result) | 1700 | files = files.concat(result) |
1701 | } | 1701 | } |
1702 | 1702 | ||
1703 | for (const p of (this.VideoStreamingPlaylists || [])) { | 1703 | for (const p of (this.VideoStreamingPlaylists || [])) { |
1704 | const result = videoFilesModelToFormattedJSON(this, p.VideoFiles, includeMagnet) | 1704 | const result = videoFilesModelToFormattedJSON(this, p.VideoFiles, { includeMagnet }) |
1705 | files = files.concat(result) | 1705 | files = files.concat(result) |
1706 | } | 1706 | } |
1707 | 1707 | ||
@@ -1868,22 +1868,14 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1868 | return setAsUpdated('video', this.id, transaction) | 1868 | return setAsUpdated('video', this.id, transaction) |
1869 | } | 1869 | } |
1870 | 1870 | ||
1871 | requiresAuth () { | 1871 | requiresAuth (paramId: string) { |
1872 | return this.privacy === VideoPrivacy.PRIVATE || this.privacy === VideoPrivacy.INTERNAL || !!this.VideoBlacklist | 1872 | if (this.privacy === VideoPrivacy.UNLISTED) { |
1873 | } | 1873 | if (!isUUIDValid(paramId)) return true |
1874 | 1874 | ||
1875 | setPrivacy (newPrivacy: VideoPrivacy) { | 1875 | return false |
1876 | if (this.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) { | ||
1877 | this.publishedAt = new Date() | ||
1878 | } | 1876 | } |
1879 | 1877 | ||
1880 | this.privacy = newPrivacy | 1878 | return this.privacy === VideoPrivacy.PRIVATE || this.privacy === VideoPrivacy.INTERNAL || !!this.VideoBlacklist |
1881 | } | ||
1882 | |||
1883 | isConfidential () { | ||
1884 | return this.privacy === VideoPrivacy.PRIVATE || | ||
1885 | this.privacy === VideoPrivacy.UNLISTED || | ||
1886 | this.privacy === VideoPrivacy.INTERNAL | ||
1887 | } | 1879 | } |
1888 | 1880 | ||
1889 | async setNewState (newState: VideoState, isNewVideo: boolean, transaction: Transaction) { | 1881 | async setNewState (newState: VideoState, isNewVideo: boolean, transaction: Transaction) { |
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index 33dc8fb76..961093bb5 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -34,6 +34,7 @@ import './video-imports' | |||
34 | import './video-playlists' | 34 | import './video-playlists' |
35 | import './video-source' | 35 | import './video-source' |
36 | import './video-studio' | 36 | import './video-studio' |
37 | import './video-token' | ||
37 | import './videos-common-filters' | 38 | import './videos-common-filters' |
38 | import './videos-history' | 39 | import './videos-history' |
39 | import './videos-overviews' | 40 | import './videos-overviews' |
diff --git a/server/tests/api/check-params/live.ts b/server/tests/api/check-params/live.ts index 3f553c42b..2eff9414b 100644 --- a/server/tests/api/check-params/live.ts +++ b/server/tests/api/check-params/live.ts | |||
@@ -502,6 +502,23 @@ describe('Test video lives API validator', function () { | |||
502 | await stopFfmpeg(ffmpegCommand) | 502 | await stopFfmpeg(ffmpegCommand) |
503 | }) | 503 | }) |
504 | 504 | ||
505 | it('Should fail to change live privacy if it has already started', async function () { | ||
506 | this.timeout(40000) | ||
507 | |||
508 | const live = await command.get({ videoId: video.id }) | ||
509 | |||
510 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
511 | |||
512 | await command.waitUntilPublished({ videoId: video.id }) | ||
513 | await server.videos.update({ | ||
514 | id: video.id, | ||
515 | attributes: { privacy: VideoPrivacy.PUBLIC }, | ||
516 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
517 | }) | ||
518 | |||
519 | await stopFfmpeg(ffmpegCommand) | ||
520 | }) | ||
521 | |||
505 | it('Should fail to stream twice in the save live', async function () { | 522 | it('Should fail to stream twice in the save live', async function () { |
506 | this.timeout(40000) | 523 | this.timeout(40000) |
507 | 524 | ||
diff --git a/server/tests/api/check-params/video-files.ts b/server/tests/api/check-params/video-files.ts index aa4de2c83..9dc59a1b5 100644 --- a/server/tests/api/check-params/video-files.ts +++ b/server/tests/api/check-params/video-files.ts | |||
@@ -1,10 +1,12 @@ | |||
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 { HttpStatusCode, UserRole } from '@shared/models' | 3 | import { getAllFiles } from '@shared/core-utils' |
4 | import { HttpStatusCode, UserRole, VideoDetails, VideoPrivacy } from '@shared/models' | ||
4 | import { | 5 | import { |
5 | cleanupTests, | 6 | cleanupTests, |
6 | createMultipleServers, | 7 | createMultipleServers, |
7 | doubleFollow, | 8 | doubleFollow, |
9 | makeRawRequest, | ||
8 | PeerTubeServer, | 10 | PeerTubeServer, |
9 | setAccessTokensToServers, | 11 | setAccessTokensToServers, |
10 | waitJobs | 12 | waitJobs |
@@ -13,22 +15,9 @@ import { | |||
13 | describe('Test videos files', function () { | 15 | describe('Test videos files', function () { |
14 | let servers: PeerTubeServer[] | 16 | let servers: PeerTubeServer[] |
15 | 17 | ||
16 | let webtorrentId: string | ||
17 | let hlsId: string | ||
18 | let remoteId: string | ||
19 | |||
20 | let userToken: string | 18 | let userToken: string |
21 | let moderatorToken: string | 19 | let moderatorToken: string |
22 | 20 | ||
23 | let validId1: string | ||
24 | let validId2: string | ||
25 | |||
26 | let hlsFileId: number | ||
27 | let webtorrentFileId: number | ||
28 | |||
29 | let remoteHLSFileId: number | ||
30 | let remoteWebtorrentFileId: number | ||
31 | |||
32 | // --------------------------------------------------------------- | 21 | // --------------------------------------------------------------- |
33 | 22 | ||
34 | before(async function () { | 23 | before(async function () { |
@@ -41,117 +30,163 @@ describe('Test videos files', function () { | |||
41 | 30 | ||
42 | userToken = await servers[0].users.generateUserAndToken('user', UserRole.USER) | 31 | userToken = await servers[0].users.generateUserAndToken('user', UserRole.USER) |
43 | moderatorToken = await servers[0].users.generateUserAndToken('moderator', UserRole.MODERATOR) | 32 | moderatorToken = await servers[0].users.generateUserAndToken('moderator', UserRole.MODERATOR) |
33 | }) | ||
44 | 34 | ||
45 | { | 35 | describe('Getting metadata', function () { |
46 | const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' }) | 36 | let video: VideoDetails |
47 | await waitJobs(servers) | 37 | |
38 | before(async function () { | ||
39 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | ||
40 | video = await servers[0].videos.getWithToken({ id: uuid }) | ||
41 | }) | ||
42 | |||
43 | it('Should not get metadata of private video without token', async function () { | ||
44 | for (const file of getAllFiles(video)) { | ||
45 | await makeRawRequest({ url: file.metadataUrl, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
46 | } | ||
47 | }) | ||
48 | |||
49 | it('Should not get metadata of private video without the appropriate token', async function () { | ||
50 | for (const file of getAllFiles(video)) { | ||
51 | await makeRawRequest({ url: file.metadataUrl, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
52 | } | ||
53 | }) | ||
54 | |||
55 | it('Should get metadata of private video with the appropriate token', async function () { | ||
56 | for (const file of getAllFiles(video)) { | ||
57 | await makeRawRequest({ url: file.metadataUrl, token: servers[0].accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
58 | } | ||
59 | }) | ||
60 | }) | ||
61 | |||
62 | describe('Deleting files', function () { | ||
63 | let webtorrentId: string | ||
64 | let hlsId: string | ||
65 | let remoteId: string | ||
66 | |||
67 | let validId1: string | ||
68 | let validId2: string | ||
48 | 69 | ||
49 | const video = await servers[1].videos.get({ id: uuid }) | 70 | let hlsFileId: number |
50 | remoteId = video.uuid | 71 | let webtorrentFileId: number |
51 | remoteHLSFileId = video.streamingPlaylists[0].files[0].id | ||
52 | remoteWebtorrentFileId = video.files[0].id | ||
53 | } | ||
54 | 72 | ||
55 | { | 73 | let remoteHLSFileId: number |
56 | await servers[0].config.enableTranscoding(true, true) | 74 | let remoteWebtorrentFileId: number |
75 | |||
76 | before(async function () { | ||
77 | this.timeout(300_000) | ||
57 | 78 | ||
58 | { | 79 | { |
59 | const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1' }) | 80 | const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' }) |
60 | await waitJobs(servers) | 81 | await waitJobs(servers) |
61 | 82 | ||
62 | const video = await servers[0].videos.get({ id: uuid }) | 83 | const video = await servers[1].videos.get({ id: uuid }) |
63 | validId1 = video.uuid | 84 | remoteId = video.uuid |
64 | hlsFileId = video.streamingPlaylists[0].files[0].id | 85 | remoteHLSFileId = video.streamingPlaylists[0].files[0].id |
65 | webtorrentFileId = video.files[0].id | 86 | remoteWebtorrentFileId = video.files[0].id |
66 | } | 87 | } |
67 | 88 | ||
68 | { | 89 | { |
69 | const { uuid } = await servers[0].videos.quickUpload({ name: 'both 2' }) | 90 | await servers[0].config.enableTranscoding(true, true) |
70 | validId2 = uuid | 91 | |
92 | { | ||
93 | const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1' }) | ||
94 | await waitJobs(servers) | ||
95 | |||
96 | const video = await servers[0].videos.get({ id: uuid }) | ||
97 | validId1 = video.uuid | ||
98 | hlsFileId = video.streamingPlaylists[0].files[0].id | ||
99 | webtorrentFileId = video.files[0].id | ||
100 | } | ||
101 | |||
102 | { | ||
103 | const { uuid } = await servers[0].videos.quickUpload({ name: 'both 2' }) | ||
104 | validId2 = uuid | ||
105 | } | ||
71 | } | 106 | } |
72 | } | ||
73 | 107 | ||
74 | await waitJobs(servers) | 108 | await waitJobs(servers) |
75 | 109 | ||
76 | { | 110 | { |
77 | await servers[0].config.enableTranscoding(false, true) | 111 | await servers[0].config.enableTranscoding(false, true) |
78 | const { uuid } = await servers[0].videos.quickUpload({ name: 'hls' }) | 112 | const { uuid } = await servers[0].videos.quickUpload({ name: 'hls' }) |
79 | hlsId = uuid | 113 | hlsId = uuid |
80 | } | 114 | } |
81 | 115 | ||
82 | await waitJobs(servers) | 116 | await waitJobs(servers) |
83 | 117 | ||
84 | { | 118 | { |
85 | await servers[0].config.enableTranscoding(false, true) | 119 | await servers[0].config.enableTranscoding(false, true) |
86 | const { uuid } = await servers[0].videos.quickUpload({ name: 'webtorrent' }) | 120 | const { uuid } = await servers[0].videos.quickUpload({ name: 'webtorrent' }) |
87 | webtorrentId = uuid | 121 | webtorrentId = uuid |
88 | } | 122 | } |
89 | 123 | ||
90 | await waitJobs(servers) | 124 | await waitJobs(servers) |
91 | }) | 125 | }) |
92 | 126 | ||
93 | it('Should not delete files of a unknown video', async function () { | 127 | it('Should not delete files of a unknown video', async function () { |
94 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 | 128 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 |
95 | 129 | ||
96 | await servers[0].videos.removeHLSPlaylist({ videoId: 404, expectedStatus }) | 130 | await servers[0].videos.removeHLSPlaylist({ videoId: 404, expectedStatus }) |
97 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: 404, expectedStatus }) | 131 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: 404, expectedStatus }) |
98 | 132 | ||
99 | await servers[0].videos.removeHLSFile({ videoId: 404, fileId: hlsFileId, expectedStatus }) | 133 | await servers[0].videos.removeHLSFile({ videoId: 404, fileId: hlsFileId, expectedStatus }) |
100 | await servers[0].videos.removeWebTorrentFile({ videoId: 404, fileId: webtorrentFileId, expectedStatus }) | 134 | await servers[0].videos.removeWebTorrentFile({ videoId: 404, fileId: webtorrentFileId, expectedStatus }) |
101 | }) | 135 | }) |
102 | 136 | ||
103 | it('Should not delete unknown files', async function () { | 137 | it('Should not delete unknown files', async function () { |
104 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 | 138 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 |
105 | 139 | ||
106 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: webtorrentFileId, expectedStatus }) | 140 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: webtorrentFileId, expectedStatus }) |
107 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: hlsFileId, expectedStatus }) | 141 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: hlsFileId, expectedStatus }) |
108 | }) | 142 | }) |
109 | 143 | ||
110 | it('Should not delete files of a remote video', async function () { | 144 | it('Should not delete files of a remote video', async function () { |
111 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | 145 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 |
112 | 146 | ||
113 | await servers[0].videos.removeHLSPlaylist({ videoId: remoteId, expectedStatus }) | 147 | await servers[0].videos.removeHLSPlaylist({ videoId: remoteId, expectedStatus }) |
114 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: remoteId, expectedStatus }) | 148 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: remoteId, expectedStatus }) |
115 | 149 | ||
116 | await servers[0].videos.removeHLSFile({ videoId: remoteId, fileId: remoteHLSFileId, expectedStatus }) | 150 | await servers[0].videos.removeHLSFile({ videoId: remoteId, fileId: remoteHLSFileId, expectedStatus }) |
117 | await servers[0].videos.removeWebTorrentFile({ videoId: remoteId, fileId: remoteWebtorrentFileId, expectedStatus }) | 151 | await servers[0].videos.removeWebTorrentFile({ videoId: remoteId, fileId: remoteWebtorrentFileId, expectedStatus }) |
118 | }) | 152 | }) |
119 | 153 | ||
120 | 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 () { |
121 | const expectedStatus = HttpStatusCode.FORBIDDEN_403 | 155 | const expectedStatus = HttpStatusCode.FORBIDDEN_403 |
122 | 156 | ||
123 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: userToken, expectedStatus }) | 157 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: userToken, expectedStatus }) |
124 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: moderatorToken, expectedStatus }) | 158 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: moderatorToken, expectedStatus }) |
125 | 159 | ||
126 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1, token: userToken, expectedStatus }) | 160 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1, token: userToken, expectedStatus }) |
127 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1, token: moderatorToken, expectedStatus }) | 161 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1, token: moderatorToken, expectedStatus }) |
128 | 162 | ||
129 | 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 }) |
130 | 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 }) |
131 | 165 | ||
132 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId, token: userToken, expectedStatus }) | 166 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId, token: userToken, expectedStatus }) |
133 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId, token: moderatorToken, expectedStatus }) | 167 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId, token: moderatorToken, expectedStatus }) |
134 | }) | 168 | }) |
135 | 169 | ||
136 | 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 () { |
137 | 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 }) |
138 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 172 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
139 | 173 | ||
140 | 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 }) |
141 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 175 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
142 | }) | 176 | }) |
143 | 177 | ||
144 | 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 () { |
145 | 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 }) |
146 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 180 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
147 | }) | 181 | }) |
148 | 182 | ||
149 | it('Should delete files if both versions are available', async function () { | 183 | it('Should delete files if both versions are available', async function () { |
150 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId }) | 184 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId }) |
151 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId }) | 185 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId }) |
152 | 186 | ||
153 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1 }) | 187 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1 }) |
154 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId2 }) | 188 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId2 }) |
189 | }) | ||
155 | }) | 190 | }) |
156 | 191 | ||
157 | after(async function () { | 192 | after(async function () { |
diff --git a/server/tests/api/check-params/video-token.ts b/server/tests/api/check-params/video-token.ts new file mode 100644 index 000000000..7acb9d580 --- /dev/null +++ b/server/tests/api/check-params/video-token.ts | |||
@@ -0,0 +1,44 @@ | |||
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 tokens', function () { | ||
7 | let server: PeerTubeServer | ||
8 | let videoId: string | ||
9 | let userToken: string | ||
10 | |||
11 | // --------------------------------------------------------------- | ||
12 | |||
13 | before(async function () { | ||
14 | this.timeout(300_000) | ||
15 | |||
16 | server = await createSingleServer(1) | ||
17 | await setAccessTokensToServers([ server ]) | ||
18 | |||
19 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | ||
20 | videoId = uuid | ||
21 | |||
22 | userToken = await server.users.generateUserAndToken('user1') | ||
23 | }) | ||
24 | |||
25 | it('Should not generate tokens for unauthenticated user', async function () { | ||
26 | await server.videoToken.create({ videoId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
27 | }) | ||
28 | |||
29 | it('Should not generate tokens of unknown video', async function () { | ||
30 | await server.videoToken.create({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
31 | }) | ||
32 | |||
33 | it('Should not generate tokens of a non owned video', async function () { | ||
34 | await server.videoToken.create({ videoId, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
35 | }) | ||
36 | |||
37 | it('Should generate token', async function () { | ||
38 | await server.videoToken.create({ videoId }) | ||
39 | }) | ||
40 | |||
41 | after(async function () { | ||
42 | await cleanupTests([ server ]) | ||
43 | }) | ||
44 | }) | ||
diff --git a/server/tests/api/live/live-fast-restream.ts b/server/tests/api/live/live-fast-restream.ts index 772ea792d..971df1a61 100644 --- a/server/tests/api/live/live-fast-restream.ts +++ b/server/tests/api/live/live-fast-restream.ts | |||
@@ -79,8 +79,8 @@ describe('Fast restream in live', function () { | |||
79 | expect(video.streamingPlaylists).to.have.lengthOf(1) | 79 | expect(video.streamingPlaylists).to.have.lengthOf(1) |
80 | 80 | ||
81 | await server.live.getSegmentFile({ videoUUID: liveId, segment: 0, playlistNumber: 0 }) | 81 | await server.live.getSegmentFile({ videoUUID: liveId, segment: 0, playlistNumber: 0 }) |
82 | await makeRawRequest(video.streamingPlaylists[0].playlistUrl, HttpStatusCode.OK_200) | 82 | await makeRawRequest({ url: video.streamingPlaylists[0].playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) |
83 | await makeRawRequest(video.streamingPlaylists[0].segmentsSha256Url, HttpStatusCode.OK_200) | 83 | await makeRawRequest({ url: video.streamingPlaylists[0].segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) |
84 | 84 | ||
85 | await wait(100) | 85 | await wait(100) |
86 | } | 86 | } |
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index 3f2a304be..003cc934f 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts | |||
@@ -21,6 +21,7 @@ import { | |||
21 | doubleFollow, | 21 | doubleFollow, |
22 | killallServers, | 22 | killallServers, |
23 | LiveCommand, | 23 | LiveCommand, |
24 | makeGetRequest, | ||
24 | makeRawRequest, | 25 | makeRawRequest, |
25 | PeerTubeServer, | 26 | PeerTubeServer, |
26 | sendRTMPStream, | 27 | sendRTMPStream, |
@@ -157,8 +158,8 @@ describe('Test live', function () { | |||
157 | expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED) | 158 | expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED) |
158 | expect(video.nsfw).to.be.true | 159 | expect(video.nsfw).to.be.true |
159 | 160 | ||
160 | await makeRawRequest(server.url + video.thumbnailPath, HttpStatusCode.OK_200) | 161 | await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) |
161 | await makeRawRequest(server.url + video.previewPath, HttpStatusCode.OK_200) | 162 | await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 }) |
162 | } | 163 | } |
163 | }) | 164 | }) |
164 | 165 | ||
@@ -532,8 +533,8 @@ describe('Test live', function () { | |||
532 | expect(video.files).to.have.lengthOf(0) | 533 | expect(video.files).to.have.lengthOf(0) |
533 | 534 | ||
534 | const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS) | 535 | const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS) |
535 | await makeRawRequest(hlsPlaylist.playlistUrl, HttpStatusCode.OK_200) | 536 | await makeRawRequest({ url: hlsPlaylist.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) |
536 | await makeRawRequest(hlsPlaylist.segmentsSha256Url, HttpStatusCode.OK_200) | 537 | await makeRawRequest({ url: hlsPlaylist.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) |
537 | 538 | ||
538 | // We should have generated random filenames | 539 | // We should have generated random filenames |
539 | expect(basename(hlsPlaylist.playlistUrl)).to.not.equal('master.m3u8') | 540 | expect(basename(hlsPlaylist.playlistUrl)).to.not.equal('master.m3u8') |
@@ -564,8 +565,8 @@ describe('Test live', function () { | |||
564 | expect(probe.format.bit_rate).to.be.below(maxBitrateLimits[videoStream.height]) | 565 | expect(probe.format.bit_rate).to.be.below(maxBitrateLimits[videoStream.height]) |
565 | expect(probe.format.bit_rate).to.be.at.least(minBitrateLimits[videoStream.height]) | 566 | expect(probe.format.bit_rate).to.be.at.least(minBitrateLimits[videoStream.height]) |
566 | 567 | ||
567 | await makeRawRequest(file.torrentUrl, HttpStatusCode.OK_200) | 568 | await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 }) |
568 | await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) | 569 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) |
569 | } | 570 | } |
570 | } | 571 | } |
571 | }) | 572 | }) |
diff --git a/server/tests/api/object-storage/live.ts b/server/tests/api/object-storage/live.ts index 7e16b4c89..77f3a8066 100644 --- a/server/tests/api/object-storage/live.ts +++ b/server/tests/api/object-storage/live.ts | |||
@@ -48,7 +48,7 @@ async function checkFilesExist (servers: PeerTubeServer[], videoUUID: string, nu | |||
48 | for (const file of files) { | 48 | for (const file of files) { |
49 | expectStartWith(file.fileUrl, ObjectStorageCommand.getPlaylistBaseUrl()) | 49 | expectStartWith(file.fileUrl, ObjectStorageCommand.getPlaylistBaseUrl()) |
50 | 50 | ||
51 | await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) | 51 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) |
52 | } | 52 | } |
53 | } | 53 | } |
54 | } | 54 | } |
diff --git a/server/tests/api/object-storage/video-imports.ts b/server/tests/api/object-storage/video-imports.ts index f688c7018..90988ea9a 100644 --- a/server/tests/api/object-storage/video-imports.ts +++ b/server/tests/api/object-storage/video-imports.ts | |||
@@ -66,7 +66,7 @@ describe('Object storage for video import', function () { | |||
66 | const fileUrl = video.files[0].fileUrl | 66 | const fileUrl = video.files[0].fileUrl |
67 | expectStartWith(fileUrl, ObjectStorageCommand.getWebTorrentBaseUrl()) | 67 | expectStartWith(fileUrl, ObjectStorageCommand.getWebTorrentBaseUrl()) |
68 | 68 | ||
69 | await makeRawRequest(fileUrl, HttpStatusCode.OK_200) | 69 | await makeRawRequest({ url: fileUrl, expectedStatus: HttpStatusCode.OK_200 }) |
70 | }) | 70 | }) |
71 | }) | 71 | }) |
72 | 72 | ||
@@ -91,13 +91,13 @@ describe('Object storage for video import', function () { | |||
91 | for (const file of video.files) { | 91 | for (const file of video.files) { |
92 | expectStartWith(file.fileUrl, ObjectStorageCommand.getWebTorrentBaseUrl()) | 92 | expectStartWith(file.fileUrl, ObjectStorageCommand.getWebTorrentBaseUrl()) |
93 | 93 | ||
94 | await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) | 94 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) |
95 | } | 95 | } |
96 | 96 | ||
97 | for (const file of video.streamingPlaylists[0].files) { | 97 | for (const file of video.streamingPlaylists[0].files) { |
98 | expectStartWith(file.fileUrl, ObjectStorageCommand.getPlaylistBaseUrl()) | 98 | expectStartWith(file.fileUrl, ObjectStorageCommand.getPlaylistBaseUrl()) |
99 | 99 | ||
100 | await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) | 100 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) |
101 | } | 101 | } |
102 | }) | 102 | }) |
103 | }) | 103 | }) |
diff --git a/server/tests/api/object-storage/videos.ts b/server/tests/api/object-storage/videos.ts index 3e65e1093..63f5179c7 100644 --- a/server/tests/api/object-storage/videos.ts +++ b/server/tests/api/object-storage/videos.ts | |||
@@ -59,11 +59,11 @@ async function checkFiles (options: { | |||
59 | 59 | ||
60 | expectStartWith(file.fileUrl, start) | 60 | expectStartWith(file.fileUrl, start) |
61 | 61 | ||
62 | const res = await makeRawRequest(file.fileDownloadUrl, HttpStatusCode.FOUND_302) | 62 | const res = await makeRawRequest({ url: file.fileDownloadUrl, expectedStatus: HttpStatusCode.FOUND_302 }) |
63 | const location = res.headers['location'] | 63 | const location = res.headers['location'] |
64 | expectStartWith(location, start) | 64 | expectStartWith(location, start) |
65 | 65 | ||
66 | await makeRawRequest(location, HttpStatusCode.OK_200) | 66 | await makeRawRequest({ url: location, expectedStatus: HttpStatusCode.OK_200 }) |
67 | } | 67 | } |
68 | 68 | ||
69 | const hls = video.streamingPlaylists[0] | 69 | const hls = video.streamingPlaylists[0] |
@@ -81,19 +81,19 @@ async function checkFiles (options: { | |||
81 | expectStartWith(hls.playlistUrl, start) | 81 | expectStartWith(hls.playlistUrl, start) |
82 | expectStartWith(hls.segmentsSha256Url, start) | 82 | expectStartWith(hls.segmentsSha256Url, start) |
83 | 83 | ||
84 | await makeRawRequest(hls.playlistUrl, HttpStatusCode.OK_200) | 84 | await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) |
85 | 85 | ||
86 | const resSha = await makeRawRequest(hls.segmentsSha256Url, HttpStatusCode.OK_200) | 86 | const resSha = await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) |
87 | expect(JSON.stringify(resSha.body)).to.not.throw | 87 | expect(JSON.stringify(resSha.body)).to.not.throw |
88 | 88 | ||
89 | for (const file of hls.files) { | 89 | for (const file of hls.files) { |
90 | expectStartWith(file.fileUrl, start) | 90 | expectStartWith(file.fileUrl, start) |
91 | 91 | ||
92 | const res = await makeRawRequest(file.fileDownloadUrl, HttpStatusCode.FOUND_302) | 92 | const res = await makeRawRequest({ url: file.fileDownloadUrl, expectedStatus: HttpStatusCode.FOUND_302 }) |
93 | const location = res.headers['location'] | 93 | const location = res.headers['location'] |
94 | expectStartWith(location, start) | 94 | expectStartWith(location, start) |
95 | 95 | ||
96 | await makeRawRequest(location, HttpStatusCode.OK_200) | 96 | await makeRawRequest({ url: location, expectedStatus: HttpStatusCode.OK_200 }) |
97 | } | 97 | } |
98 | } | 98 | } |
99 | 99 | ||
@@ -104,7 +104,7 @@ async function checkFiles (options: { | |||
104 | expect(torrent.files.length).to.equal(1) | 104 | expect(torrent.files.length).to.equal(1) |
105 | expect(torrent.files[0].path).to.exist.and.to.not.equal('') | 105 | expect(torrent.files[0].path).to.exist.and.to.not.equal('') |
106 | 106 | ||
107 | const res = await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) | 107 | const res = await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) |
108 | expect(res.body).to.have.length.above(100) | 108 | expect(res.body).to.have.length.above(100) |
109 | } | 109 | } |
110 | 110 | ||
@@ -220,7 +220,7 @@ function runTestSuite (options: { | |||
220 | 220 | ||
221 | it('Should fetch correctly all the files', async function () { | 221 | it('Should fetch correctly all the files', async function () { |
222 | for (const url of deletedUrls.concat(keptUrls)) { | 222 | for (const url of deletedUrls.concat(keptUrls)) { |
223 | await makeRawRequest(url, HttpStatusCode.OK_200) | 223 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) |
224 | } | 224 | } |
225 | }) | 225 | }) |
226 | 226 | ||
@@ -231,13 +231,13 @@ function runTestSuite (options: { | |||
231 | await waitJobs(servers) | 231 | await waitJobs(servers) |
232 | 232 | ||
233 | for (const url of deletedUrls) { | 233 | for (const url of deletedUrls) { |
234 | await makeRawRequest(url, HttpStatusCode.NOT_FOUND_404) | 234 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
235 | } | 235 | } |
236 | }) | 236 | }) |
237 | 237 | ||
238 | it('Should have kept other files', async function () { | 238 | it('Should have kept other files', async function () { |
239 | for (const url of keptUrls) { | 239 | for (const url of keptUrls) { |
240 | await makeRawRequest(url, HttpStatusCode.OK_200) | 240 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) |
241 | } | 241 | } |
242 | }) | 242 | }) |
243 | 243 | ||
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts index f349a7a76..ba6b00e0b 100644 --- a/server/tests/api/redundancy/redundancy.ts +++ b/server/tests/api/redundancy/redundancy.ts | |||
@@ -39,7 +39,7 @@ async function checkMagnetWebseeds (file: VideoFile, baseWebseeds: string[], ser | |||
39 | expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length) | 39 | expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length) |
40 | 40 | ||
41 | for (const url of parsed.urlList) { | 41 | for (const url of parsed.urlList) { |
42 | await makeRawRequest(url, HttpStatusCode.OK_200) | 42 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) |
43 | } | 43 | } |
44 | } | 44 | } |
45 | 45 | ||
diff --git a/server/tests/api/server/open-telemetry.ts b/server/tests/api/server/open-telemetry.ts index 43a27cc32..7a294be82 100644 --- a/server/tests/api/server/open-telemetry.ts +++ b/server/tests/api/server/open-telemetry.ts | |||
@@ -18,7 +18,7 @@ describe('Open Telemetry', function () { | |||
18 | 18 | ||
19 | let hasError = false | 19 | let hasError = false |
20 | try { | 20 | try { |
21 | await makeRawRequest(metricsUrl, HttpStatusCode.NOT_FOUND_404) | 21 | await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
22 | } catch (err) { | 22 | } catch (err) { |
23 | hasError = err.message.includes('ECONNREFUSED') | 23 | hasError = err.message.includes('ECONNREFUSED') |
24 | } | 24 | } |
@@ -37,7 +37,7 @@ describe('Open Telemetry', function () { | |||
37 | } | 37 | } |
38 | }) | 38 | }) |
39 | 39 | ||
40 | const res = await makeRawRequest(metricsUrl, HttpStatusCode.OK_200) | 40 | const res = await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.OK_200 }) |
41 | expect(res.text).to.contain('peertube_job_queue_total{') | 41 | expect(res.text).to.contain('peertube_job_queue_total{') |
42 | }) | 42 | }) |
43 | 43 | ||
@@ -60,7 +60,7 @@ describe('Open Telemetry', function () { | |||
60 | } | 60 | } |
61 | }) | 61 | }) |
62 | 62 | ||
63 | const res = await makeRawRequest(metricsUrl, HttpStatusCode.OK_200) | 63 | const res = await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.OK_200 }) |
64 | expect(res.text).to.contain('peertube_playback_http_downloaded_bytes_total{') | 64 | expect(res.text).to.contain('peertube_playback_http_downloaded_bytes_total{') |
65 | }) | 65 | }) |
66 | 66 | ||
diff --git a/server/tests/api/transcoding/create-transcoding.ts b/server/tests/api/transcoding/create-transcoding.ts index a50bf7654..372f5332a 100644 --- a/server/tests/api/transcoding/create-transcoding.ts +++ b/server/tests/api/transcoding/create-transcoding.ts | |||
@@ -20,7 +20,7 @@ import { | |||
20 | async function checkFilesInObjectStorage (video: VideoDetails) { | 20 | async function checkFilesInObjectStorage (video: VideoDetails) { |
21 | for (const file of video.files) { | 21 | for (const file of video.files) { |
22 | expectStartWith(file.fileUrl, ObjectStorageCommand.getWebTorrentBaseUrl()) | 22 | expectStartWith(file.fileUrl, ObjectStorageCommand.getWebTorrentBaseUrl()) |
23 | await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) | 23 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) |
24 | } | 24 | } |
25 | 25 | ||
26 | if (video.streamingPlaylists.length === 0) return | 26 | if (video.streamingPlaylists.length === 0) return |
@@ -28,14 +28,14 @@ async function checkFilesInObjectStorage (video: VideoDetails) { | |||
28 | const hlsPlaylist = video.streamingPlaylists[0] | 28 | const hlsPlaylist = video.streamingPlaylists[0] |
29 | for (const file of hlsPlaylist.files) { | 29 | for (const file of hlsPlaylist.files) { |
30 | expectStartWith(file.fileUrl, ObjectStorageCommand.getPlaylistBaseUrl()) | 30 | expectStartWith(file.fileUrl, ObjectStorageCommand.getPlaylistBaseUrl()) |
31 | await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) | 31 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) |
32 | } | 32 | } |
33 | 33 | ||
34 | expectStartWith(hlsPlaylist.playlistUrl, ObjectStorageCommand.getPlaylistBaseUrl()) | 34 | expectStartWith(hlsPlaylist.playlistUrl, ObjectStorageCommand.getPlaylistBaseUrl()) |
35 | await makeRawRequest(hlsPlaylist.playlistUrl, HttpStatusCode.OK_200) | 35 | await makeRawRequest({ url: hlsPlaylist.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) |
36 | 36 | ||
37 | expectStartWith(hlsPlaylist.segmentsSha256Url, ObjectStorageCommand.getPlaylistBaseUrl()) | 37 | expectStartWith(hlsPlaylist.segmentsSha256Url, ObjectStorageCommand.getPlaylistBaseUrl()) |
38 | await makeRawRequest(hlsPlaylist.segmentsSha256Url, HttpStatusCode.OK_200) | 38 | await makeRawRequest({ url: hlsPlaylist.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) |
39 | } | 39 | } |
40 | 40 | ||
41 | function runTests (objectStorage: boolean) { | 41 | function runTests (objectStorage: boolean) { |
@@ -234,7 +234,7 @@ function runTests (objectStorage: boolean) { | |||
234 | 234 | ||
235 | it('Should have correctly deleted previous files', async function () { | 235 | it('Should have correctly deleted previous files', async function () { |
236 | for (const fileUrl of shouldBeDeleted) { | 236 | for (const fileUrl of shouldBeDeleted) { |
237 | await makeRawRequest(fileUrl, HttpStatusCode.NOT_FOUND_404) | 237 | await makeRawRequest({ url: fileUrl, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
238 | } | 238 | } |
239 | }) | 239 | }) |
240 | 240 | ||
diff --git a/server/tests/api/transcoding/hls.ts b/server/tests/api/transcoding/hls.ts index 252422e5d..7b5492cd4 100644 --- a/server/tests/api/transcoding/hls.ts +++ b/server/tests/api/transcoding/hls.ts | |||
@@ -1,168 +1,48 @@ | |||
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 { join } from 'path' |
4 | import { basename, join } from 'path' | 4 | import { checkDirectoryIsEmpty, checkTmpIsEmpty, completeCheckHlsPlaylist } from '@server/tests/shared' |
5 | import { | 5 | import { areObjectStorageTestsDisabled } from '@shared/core-utils' |
6 | checkDirectoryIsEmpty, | 6 | import { HttpStatusCode } from '@shared/models' |
7 | checkResolutionsInMasterPlaylist, | ||
8 | checkSegmentHash, | ||
9 | checkTmpIsEmpty, | ||
10 | expectStartWith, | ||
11 | hlsInfohashExist | ||
12 | } from '@server/tests/shared' | ||
13 | import { areObjectStorageTestsDisabled, removeFragmentedMP4Ext, uuidRegex } from '@shared/core-utils' | ||
14 | import { HttpStatusCode, VideoStreamingPlaylistType } from '@shared/models' | ||
15 | import { | 7 | import { |
16 | cleanupTests, | 8 | cleanupTests, |
17 | createMultipleServers, | 9 | createMultipleServers, |
18 | doubleFollow, | 10 | doubleFollow, |
19 | makeRawRequest, | ||
20 | ObjectStorageCommand, | 11 | ObjectStorageCommand, |
21 | PeerTubeServer, | 12 | PeerTubeServer, |
22 | setAccessTokensToServers, | 13 | setAccessTokensToServers, |
23 | waitJobs, | 14 | waitJobs |
24 | webtorrentAdd | ||
25 | } from '@shared/server-commands' | 15 | } from '@shared/server-commands' |
26 | import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants' | 16 | import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants' |
27 | 17 | ||
28 | async function checkHlsPlaylist (options: { | ||
29 | servers: PeerTubeServer[] | ||
30 | videoUUID: string | ||
31 | hlsOnly: boolean | ||
32 | |||
33 | resolutions?: number[] | ||
34 | objectStorageBaseUrl: string | ||
35 | }) { | ||
36 | const { videoUUID, hlsOnly, objectStorageBaseUrl } = options | ||
37 | |||
38 | const resolutions = options.resolutions ?? [ 240, 360, 480, 720 ] | ||
39 | |||
40 | for (const server of options.servers) { | ||
41 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
42 | const baseUrl = `http://${videoDetails.account.host}` | ||
43 | |||
44 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
45 | |||
46 | const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
47 | expect(hlsPlaylist).to.not.be.undefined | ||
48 | |||
49 | const hlsFiles = hlsPlaylist.files | ||
50 | expect(hlsFiles).to.have.lengthOf(resolutions.length) | ||
51 | |||
52 | if (hlsOnly) expect(videoDetails.files).to.have.lengthOf(0) | ||
53 | else expect(videoDetails.files).to.have.lengthOf(resolutions.length) | ||
54 | |||
55 | // Check JSON files | ||
56 | for (const resolution of resolutions) { | ||
57 | const file = hlsFiles.find(f => f.resolution.id === resolution) | ||
58 | expect(file).to.not.be.undefined | ||
59 | |||
60 | expect(file.magnetUri).to.have.lengthOf.above(2) | ||
61 | expect(file.torrentUrl).to.match( | ||
62 | new RegExp(`http://${server.host}/lazy-static/torrents/${uuidRegex}-${file.resolution.id}-hls.torrent`) | ||
63 | ) | ||
64 | |||
65 | if (objectStorageBaseUrl) { | ||
66 | expectStartWith(file.fileUrl, objectStorageBaseUrl) | ||
67 | } else { | ||
68 | expect(file.fileUrl).to.match( | ||
69 | new RegExp(`${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${uuidRegex}-${file.resolution.id}-fragmented.mp4`) | ||
70 | ) | ||
71 | } | ||
72 | |||
73 | expect(file.resolution.label).to.equal(resolution + 'p') | ||
74 | |||
75 | await makeRawRequest(file.torrentUrl, HttpStatusCode.OK_200) | ||
76 | await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) | ||
77 | |||
78 | const torrent = await webtorrentAdd(file.magnetUri, true) | ||
79 | expect(torrent.files).to.be.an('array') | ||
80 | expect(torrent.files.length).to.equal(1) | ||
81 | expect(torrent.files[0].path).to.exist.and.to.not.equal('') | ||
82 | } | ||
83 | |||
84 | // Check master playlist | ||
85 | { | ||
86 | await checkResolutionsInMasterPlaylist({ server, playlistUrl: hlsPlaylist.playlistUrl, resolutions }) | ||
87 | |||
88 | const masterPlaylist = await server.streamingPlaylists.get({ url: hlsPlaylist.playlistUrl }) | ||
89 | |||
90 | let i = 0 | ||
91 | for (const resolution of resolutions) { | ||
92 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | ||
93 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | ||
94 | |||
95 | const url = 'http://' + videoDetails.account.host | ||
96 | await hlsInfohashExist(url, hlsPlaylist.playlistUrl, i) | ||
97 | |||
98 | i++ | ||
99 | } | ||
100 | } | ||
101 | |||
102 | // Check resolution playlists | ||
103 | { | ||
104 | for (const resolution of resolutions) { | ||
105 | const file = hlsFiles.find(f => f.resolution.id === resolution) | ||
106 | const playlistName = removeFragmentedMP4Ext(basename(file.fileUrl)) + '.m3u8' | ||
107 | |||
108 | const url = objectStorageBaseUrl | ||
109 | ? `${objectStorageBaseUrl}hls/${videoUUID}/${playlistName}` | ||
110 | : `${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${playlistName}` | ||
111 | |||
112 | const subPlaylist = await server.streamingPlaylists.get({ url }) | ||
113 | |||
114 | expect(subPlaylist).to.match(new RegExp(`${uuidRegex}-${resolution}-fragmented.mp4`)) | ||
115 | expect(subPlaylist).to.contain(basename(file.fileUrl)) | ||
116 | } | ||
117 | } | ||
118 | |||
119 | { | ||
120 | const baseUrlAndPath = objectStorageBaseUrl | ||
121 | ? objectStorageBaseUrl + 'hls/' + videoUUID | ||
122 | : baseUrl + '/static/streaming-playlists/hls/' + videoUUID | ||
123 | |||
124 | for (const resolution of resolutions) { | ||
125 | await checkSegmentHash({ | ||
126 | server, | ||
127 | baseUrlPlaylist: baseUrlAndPath, | ||
128 | baseUrlSegment: baseUrlAndPath, | ||
129 | resolution, | ||
130 | hlsPlaylist | ||
131 | }) | ||
132 | } | ||
133 | } | ||
134 | } | ||
135 | } | ||
136 | |||
137 | describe('Test HLS videos', function () { | 18 | describe('Test HLS videos', function () { |
138 | let servers: PeerTubeServer[] = [] | 19 | let servers: PeerTubeServer[] = [] |
139 | let videoUUID = '' | ||
140 | let videoAudioUUID = '' | ||
141 | 20 | ||
142 | function runTestSuite (hlsOnly: boolean, objectStorageBaseUrl?: string) { | 21 | function runTestSuite (hlsOnly: boolean, objectStorageBaseUrl?: string) { |
22 | const videoUUIDs: string[] = [] | ||
143 | 23 | ||
144 | it('Should upload a video and transcode it to HLS', async function () { | 24 | it('Should upload a video and transcode it to HLS', async function () { |
145 | this.timeout(120000) | 25 | this.timeout(120000) |
146 | 26 | ||
147 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 1', fixture: 'video_short.webm' } }) | 27 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 1', fixture: 'video_short.webm' } }) |
148 | videoUUID = uuid | 28 | videoUUIDs.push(uuid) |
149 | 29 | ||
150 | await waitJobs(servers) | 30 | await waitJobs(servers) |
151 | 31 | ||
152 | await checkHlsPlaylist({ servers, videoUUID, hlsOnly, objectStorageBaseUrl }) | 32 | await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl }) |
153 | }) | 33 | }) |
154 | 34 | ||
155 | it('Should upload an audio file and transcode it to HLS', async function () { | 35 | it('Should upload an audio file and transcode it to HLS', async function () { |
156 | this.timeout(120000) | 36 | this.timeout(120000) |
157 | 37 | ||
158 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video audio', fixture: 'sample.ogg' } }) | 38 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video audio', fixture: 'sample.ogg' } }) |
159 | videoAudioUUID = uuid | 39 | videoUUIDs.push(uuid) |
160 | 40 | ||
161 | await waitJobs(servers) | 41 | await waitJobs(servers) |
162 | 42 | ||
163 | await checkHlsPlaylist({ | 43 | await completeCheckHlsPlaylist({ |
164 | servers, | 44 | servers, |
165 | videoUUID: videoAudioUUID, | 45 | videoUUID: uuid, |
166 | hlsOnly, | 46 | hlsOnly, |
167 | resolutions: [ DEFAULT_AUDIO_RESOLUTION, 360, 240 ], | 47 | resolutions: [ DEFAULT_AUDIO_RESOLUTION, 360, 240 ], |
168 | objectStorageBaseUrl | 48 | objectStorageBaseUrl |
@@ -172,31 +52,36 @@ describe('Test HLS videos', function () { | |||
172 | it('Should update the video', async function () { | 52 | it('Should update the video', async function () { |
173 | this.timeout(30000) | 53 | this.timeout(30000) |
174 | 54 | ||
175 | await servers[0].videos.update({ id: videoUUID, attributes: { name: 'video 1 updated' } }) | 55 | await servers[0].videos.update({ id: videoUUIDs[0], attributes: { name: 'video 1 updated' } }) |
176 | 56 | ||
177 | await waitJobs(servers) | 57 | await waitJobs(servers) |
178 | 58 | ||
179 | await checkHlsPlaylist({ servers, videoUUID, hlsOnly, objectStorageBaseUrl }) | 59 | await completeCheckHlsPlaylist({ servers, videoUUID: videoUUIDs[0], hlsOnly, objectStorageBaseUrl }) |
180 | }) | 60 | }) |
181 | 61 | ||
182 | it('Should delete videos', async function () { | 62 | it('Should delete videos', async function () { |
183 | this.timeout(10000) | 63 | this.timeout(10000) |
184 | 64 | ||
185 | await servers[0].videos.remove({ id: videoUUID }) | 65 | for (const uuid of videoUUIDs) { |
186 | await servers[0].videos.remove({ id: videoAudioUUID }) | 66 | await servers[0].videos.remove({ id: uuid }) |
67 | } | ||
187 | 68 | ||
188 | await waitJobs(servers) | 69 | await waitJobs(servers) |
189 | 70 | ||
190 | for (const server of servers) { | 71 | for (const server of servers) { |
191 | await server.videos.get({ id: videoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 72 | for (const uuid of videoUUIDs) { |
192 | await server.videos.get({ id: videoAudioUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 73 | await server.videos.get({ id: uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
74 | } | ||
193 | } | 75 | } |
194 | }) | 76 | }) |
195 | 77 | ||
196 | it('Should have the playlists/segment deleted from the disk', async function () { | 78 | it('Should have the playlists/segment deleted from the disk', async function () { |
197 | for (const server of servers) { | 79 | for (const server of servers) { |
198 | await checkDirectoryIsEmpty(server, 'videos') | 80 | await checkDirectoryIsEmpty(server, 'videos', [ 'private' ]) |
199 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls')) | 81 | await checkDirectoryIsEmpty(server, join('videos', 'private')) |
82 | |||
83 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls'), [ 'private' ]) | ||
84 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls', 'private')) | ||
200 | } | 85 | } |
201 | }) | 86 | }) |
202 | 87 | ||
diff --git a/server/tests/api/transcoding/index.ts b/server/tests/api/transcoding/index.ts index 0cc28b4a4..9866418d6 100644 --- a/server/tests/api/transcoding/index.ts +++ b/server/tests/api/transcoding/index.ts | |||
@@ -2,4 +2,5 @@ export * from './audio-only' | |||
2 | export * from './create-transcoding' | 2 | export * from './create-transcoding' |
3 | export * from './hls' | 3 | export * from './hls' |
4 | export * from './transcoder' | 4 | export * from './transcoder' |
5 | export * from './update-while-transcoding' | ||
5 | export * from './video-studio' | 6 | export * from './video-studio' |
diff --git a/server/tests/api/transcoding/update-while-transcoding.ts b/server/tests/api/transcoding/update-while-transcoding.ts new file mode 100644 index 000000000..5ca923392 --- /dev/null +++ b/server/tests/api/transcoding/update-while-transcoding.ts | |||
@@ -0,0 +1,151 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { completeCheckHlsPlaylist } from '@server/tests/shared' | ||
4 | import { areObjectStorageTestsDisabled, wait } from '@shared/core-utils' | ||
5 | import { VideoPrivacy } from '@shared/models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | ObjectStorageCommand, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | waitJobs | ||
14 | } from '@shared/server-commands' | ||
15 | |||
16 | describe('Test update video privacy while transcoding', function () { | ||
17 | let servers: PeerTubeServer[] = [] | ||
18 | |||
19 | const videoUUIDs: string[] = [] | ||
20 | |||
21 | function runTestSuite (hlsOnly: boolean, objectStorageBaseUrl?: string) { | ||
22 | |||
23 | it('Should not have an error while quickly updating a private video to public after upload #1', async function () { | ||
24 | this.timeout(360_000) | ||
25 | |||
26 | const attributes = { | ||
27 | name: 'quick update', | ||
28 | privacy: VideoPrivacy.PRIVATE | ||
29 | } | ||
30 | |||
31 | const { uuid } = await servers[0].videos.upload({ attributes, waitTorrentGeneration: false }) | ||
32 | await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
33 | videoUUIDs.push(uuid) | ||
34 | |||
35 | await waitJobs(servers) | ||
36 | |||
37 | await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl }) | ||
38 | }) | ||
39 | |||
40 | it('Should not have an error while quickly updating a private video to public after upload #2', async function () { | ||
41 | |||
42 | { | ||
43 | const attributes = { | ||
44 | name: 'quick update 2', | ||
45 | privacy: VideoPrivacy.PRIVATE | ||
46 | } | ||
47 | |||
48 | const { uuid } = await servers[0].videos.upload({ attributes, waitTorrentGeneration: true }) | ||
49 | await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
50 | videoUUIDs.push(uuid) | ||
51 | |||
52 | await waitJobs(servers) | ||
53 | |||
54 | await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl }) | ||
55 | } | ||
56 | }) | ||
57 | |||
58 | it('Should not have an error while quickly updating a private video to public after upload #3', async function () { | ||
59 | const attributes = { | ||
60 | name: 'quick update 3', | ||
61 | privacy: VideoPrivacy.PRIVATE | ||
62 | } | ||
63 | |||
64 | const { uuid } = await servers[0].videos.upload({ attributes, waitTorrentGeneration: true }) | ||
65 | await wait(1000) | ||
66 | await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
67 | videoUUIDs.push(uuid) | ||
68 | |||
69 | await waitJobs(servers) | ||
70 | |||
71 | await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl }) | ||
72 | }) | ||
73 | } | ||
74 | |||
75 | before(async function () { | ||
76 | this.timeout(120000) | ||
77 | |||
78 | const configOverride = { | ||
79 | transcoding: { | ||
80 | enabled: true, | ||
81 | allow_audio_files: true, | ||
82 | hls: { | ||
83 | enabled: true | ||
84 | } | ||
85 | } | ||
86 | } | ||
87 | servers = await createMultipleServers(2, configOverride) | ||
88 | |||
89 | // Get the access tokens | ||
90 | await setAccessTokensToServers(servers) | ||
91 | |||
92 | // Server 1 and server 2 follow each other | ||
93 | await doubleFollow(servers[0], servers[1]) | ||
94 | }) | ||
95 | |||
96 | describe('With WebTorrent & HLS enabled', function () { | ||
97 | runTestSuite(false) | ||
98 | }) | ||
99 | |||
100 | describe('With only HLS enabled', function () { | ||
101 | |||
102 | before(async function () { | ||
103 | await servers[0].config.updateCustomSubConfig({ | ||
104 | newConfig: { | ||
105 | transcoding: { | ||
106 | enabled: true, | ||
107 | allowAudioFiles: true, | ||
108 | resolutions: { | ||
109 | '144p': false, | ||
110 | '240p': true, | ||
111 | '360p': true, | ||
112 | '480p': true, | ||
113 | '720p': true, | ||
114 | '1080p': true, | ||
115 | '1440p': true, | ||
116 | '2160p': true | ||
117 | }, | ||
118 | hls: { | ||
119 | enabled: true | ||
120 | }, | ||
121 | webtorrent: { | ||
122 | enabled: false | ||
123 | } | ||
124 | } | ||
125 | } | ||
126 | }) | ||
127 | }) | ||
128 | |||
129 | runTestSuite(true) | ||
130 | }) | ||
131 | |||
132 | describe('With object storage enabled', function () { | ||
133 | if (areObjectStorageTestsDisabled()) return | ||
134 | |||
135 | before(async function () { | ||
136 | this.timeout(120000) | ||
137 | |||
138 | const configOverride = ObjectStorageCommand.getDefaultConfig() | ||
139 | await ObjectStorageCommand.prepareDefaultBuckets() | ||
140 | |||
141 | await servers[0].kill() | ||
142 | await servers[0].run(configOverride) | ||
143 | }) | ||
144 | |||
145 | runTestSuite(true, ObjectStorageCommand.getPlaylistBaseUrl()) | ||
146 | }) | ||
147 | |||
148 | after(async function () { | ||
149 | await cleanupTests(servers) | ||
150 | }) | ||
151 | }) | ||
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts index 266155297..357c08199 100644 --- a/server/tests/api/videos/index.ts +++ b/server/tests/api/videos/index.ts | |||
@@ -19,3 +19,4 @@ import './videos-common-filters' | |||
19 | import './videos-history' | 19 | 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' | ||
diff --git a/server/tests/api/videos/video-files.ts b/server/tests/api/videos/video-files.ts index c0b886aad..8c913bf31 100644 --- a/server/tests/api/videos/video-files.ts +++ b/server/tests/api/videos/video-files.ts | |||
@@ -153,7 +153,7 @@ describe('Test videos files', function () { | |||
153 | expect(video.streamingPlaylists[0].files).to.have.lengthOf(files.length - 1) | 153 | expect(video.streamingPlaylists[0].files).to.have.lengthOf(files.length - 1) |
154 | expect(video.streamingPlaylists[0].files.find(f => f.id === toDelete.id)).to.not.exist | 154 | expect(video.streamingPlaylists[0].files.find(f => f.id === toDelete.id)).to.not.exist |
155 | 155 | ||
156 | const { text } = await makeRawRequest(video.streamingPlaylists[0].playlistUrl) | 156 | const { text } = await makeRawRequest({ url: video.streamingPlaylists[0].playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) |
157 | 157 | ||
158 | expect(text.includes(`-${toDelete.resolution.id}.m3u8`)).to.be.false | 158 | expect(text.includes(`-${toDelete.resolution.id}.m3u8`)).to.be.false |
159 | expect(text.includes(`-${video.streamingPlaylists[0].files[0].resolution.id}.m3u8`)).to.be.true | 159 | expect(text.includes(`-${video.streamingPlaylists[0].files[0].resolution.id}.m3u8`)).to.be.true |
diff --git a/server/tests/api/videos/video-static-file-privacy.ts b/server/tests/api/videos/video-static-file-privacy.ts new file mode 100644 index 000000000..e38fdec6e --- /dev/null +++ b/server/tests/api/videos/video-static-file-privacy.ts | |||
@@ -0,0 +1,389 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { decode } from 'magnet-uri' | ||
5 | import { expectStartWith } from '@server/tests/shared' | ||
6 | import { getAllFiles, wait } from '@shared/core-utils' | ||
7 | import { HttpStatusCode, LiveVideo, VideoDetails, VideoPrivacy } from '@shared/models' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createSingleServer, | ||
11 | findExternalSavedVideo, | ||
12 | makeRawRequest, | ||
13 | parseTorrentVideo, | ||
14 | PeerTubeServer, | ||
15 | sendRTMPStream, | ||
16 | setAccessTokensToServers, | ||
17 | setDefaultVideoChannel, | ||
18 | stopFfmpeg, | ||
19 | waitJobs | ||
20 | } from '@shared/server-commands' | ||
21 | |||
22 | describe('Test video static file privacy', function () { | ||
23 | let server: PeerTubeServer | ||
24 | let userToken: string | ||
25 | |||
26 | before(async function () { | ||
27 | this.timeout(50000) | ||
28 | |||
29 | server = await createSingleServer(1) | ||
30 | await setAccessTokensToServers([ server ]) | ||
31 | await setDefaultVideoChannel([ server ]) | ||
32 | |||
33 | userToken = await server.users.generateUserAndToken('user1') | ||
34 | }) | ||
35 | |||
36 | describe('VOD static file path', function () { | ||
37 | |||
38 | function runSuite () { | ||
39 | |||
40 | async function checkPrivateWebTorrentFiles (uuid: string) { | ||
41 | const video = await server.videos.getWithToken({ id: uuid }) | ||
42 | |||
43 | for (const file of video.files) { | ||
44 | expect(file.fileDownloadUrl).to.not.include('/private/') | ||
45 | expectStartWith(file.fileUrl, server.url + '/static/webseed/private/') | ||
46 | |||
47 | const torrent = await parseTorrentVideo(server, file) | ||
48 | expect(torrent.urlList).to.have.lengthOf(0) | ||
49 | |||
50 | const magnet = decode(file.magnetUri) | ||
51 | expect(magnet.urlList).to.have.lengthOf(0) | ||
52 | |||
53 | await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
54 | } | ||
55 | |||
56 | const hls = video.streamingPlaylists[0] | ||
57 | if (hls) { | ||
58 | expectStartWith(hls.playlistUrl, server.url + '/static/streaming-playlists/hls/private/') | ||
59 | expectStartWith(hls.segmentsSha256Url, server.url + '/static/streaming-playlists/hls/private/') | ||
60 | |||
61 | await makeRawRequest({ url: hls.playlistUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
62 | await makeRawRequest({ url: hls.segmentsSha256Url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
63 | } | ||
64 | } | ||
65 | |||
66 | async function checkPublicWebTorrentFiles (uuid: string) { | ||
67 | const video = await server.videos.get({ id: uuid }) | ||
68 | |||
69 | for (const file of getAllFiles(video)) { | ||
70 | expect(file.fileDownloadUrl).to.not.include('/private/') | ||
71 | expect(file.fileUrl).to.not.include('/private/') | ||
72 | |||
73 | const torrent = await parseTorrentVideo(server, file) | ||
74 | expect(torrent.urlList[0]).to.not.include('private') | ||
75 | |||
76 | const magnet = decode(file.magnetUri) | ||
77 | expect(magnet.urlList[0]).to.not.include('private') | ||
78 | |||
79 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
80 | await makeRawRequest({ url: torrent.urlList[0], expectedStatus: HttpStatusCode.OK_200 }) | ||
81 | await makeRawRequest({ url: magnet.urlList[0], expectedStatus: HttpStatusCode.OK_200 }) | ||
82 | } | ||
83 | |||
84 | const hls = video.streamingPlaylists[0] | ||
85 | if (hls) { | ||
86 | expect(hls.playlistUrl).to.not.include('private') | ||
87 | expect(hls.segmentsSha256Url).to.not.include('private') | ||
88 | |||
89 | await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
90 | await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) | ||
91 | } | ||
92 | } | ||
93 | |||
94 | it('Should upload a private/internal video and have a private static path', async function () { | ||
95 | this.timeout(120000) | ||
96 | |||
97 | for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { | ||
98 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy }) | ||
99 | await waitJobs([ server ]) | ||
100 | |||
101 | await checkPrivateWebTorrentFiles(uuid) | ||
102 | } | ||
103 | }) | ||
104 | |||
105 | it('Should upload a public video and update it as private/internal to have a private static path', async function () { | ||
106 | this.timeout(120000) | ||
107 | |||
108 | for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { | ||
109 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PUBLIC }) | ||
110 | await waitJobs([ server ]) | ||
111 | |||
112 | await server.videos.update({ id: uuid, attributes: { privacy } }) | ||
113 | await waitJobs([ server ]) | ||
114 | |||
115 | await checkPrivateWebTorrentFiles(uuid) | ||
116 | } | ||
117 | }) | ||
118 | |||
119 | it('Should upload a private video and update it to unlisted to have a public static path', async function () { | ||
120 | this.timeout(120000) | ||
121 | |||
122 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | ||
123 | await waitJobs([ server ]) | ||
124 | |||
125 | await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.UNLISTED } }) | ||
126 | await waitJobs([ server ]) | ||
127 | |||
128 | await checkPublicWebTorrentFiles(uuid) | ||
129 | }) | ||
130 | |||
131 | it('Should upload an internal video and update it to public to have a public static path', async function () { | ||
132 | this.timeout(120000) | ||
133 | |||
134 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.INTERNAL }) | ||
135 | await waitJobs([ server ]) | ||
136 | |||
137 | await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
138 | await waitJobs([ server ]) | ||
139 | |||
140 | await checkPublicWebTorrentFiles(uuid) | ||
141 | }) | ||
142 | |||
143 | it('Should upload an internal video and schedule a public publish', async function () { | ||
144 | this.timeout(120000) | ||
145 | |||
146 | const attributes = { | ||
147 | name: 'video', | ||
148 | privacy: VideoPrivacy.PRIVATE, | ||
149 | scheduleUpdate: { | ||
150 | updateAt: new Date(Date.now() + 1000).toISOString(), | ||
151 | privacy: VideoPrivacy.PUBLIC as VideoPrivacy.PUBLIC | ||
152 | } | ||
153 | } | ||
154 | |||
155 | const { uuid } = await server.videos.upload({ attributes }) | ||
156 | |||
157 | await waitJobs([ server ]) | ||
158 | await wait(1000) | ||
159 | await server.debug.sendCommand({ body: { command: 'process-update-videos-scheduler' } }) | ||
160 | |||
161 | await waitJobs([ server ]) | ||
162 | |||
163 | await checkPublicWebTorrentFiles(uuid) | ||
164 | }) | ||
165 | } | ||
166 | |||
167 | describe('Without transcoding', function () { | ||
168 | runSuite() | ||
169 | }) | ||
170 | |||
171 | describe('With transcoding', function () { | ||
172 | |||
173 | before(async function () { | ||
174 | await server.config.enableMinimumTranscoding() | ||
175 | }) | ||
176 | |||
177 | runSuite() | ||
178 | }) | ||
179 | }) | ||
180 | |||
181 | describe('VOD static file right check', function () { | ||
182 | let unrelatedFileToken: string | ||
183 | |||
184 | async function checkVideoFiles (options: { | ||
185 | id: string | ||
186 | expectedStatus: HttpStatusCode | ||
187 | token: string | ||
188 | videoFileToken: string | ||
189 | }) { | ||
190 | const { id, expectedStatus, token, videoFileToken } = options | ||
191 | |||
192 | const video = await server.videos.getWithToken({ id }) | ||
193 | |||
194 | for (const file of getAllFiles(video)) { | ||
195 | await makeRawRequest({ url: file.fileUrl, token, expectedStatus }) | ||
196 | await makeRawRequest({ url: file.fileDownloadUrl, token, expectedStatus }) | ||
197 | |||
198 | await makeRawRequest({ url: file.fileUrl, query: { videoFileToken }, expectedStatus }) | ||
199 | await makeRawRequest({ url: file.fileDownloadUrl, query: { videoFileToken }, expectedStatus }) | ||
200 | } | ||
201 | |||
202 | const hls = video.streamingPlaylists[0] | ||
203 | await makeRawRequest({ url: hls.playlistUrl, token, expectedStatus }) | ||
204 | await makeRawRequest({ url: hls.segmentsSha256Url, token, expectedStatus }) | ||
205 | |||
206 | await makeRawRequest({ url: hls.playlistUrl, query: { videoFileToken }, expectedStatus }) | ||
207 | await makeRawRequest({ url: hls.segmentsSha256Url, query: { videoFileToken }, expectedStatus }) | ||
208 | } | ||
209 | |||
210 | before(async function () { | ||
211 | await server.config.enableMinimumTranscoding() | ||
212 | |||
213 | const { uuid } = await server.videos.quickUpload({ name: 'another video' }) | ||
214 | unrelatedFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) | ||
215 | }) | ||
216 | |||
217 | it('Should not be able to access a private video files without OAuth token and file token', async function () { | ||
218 | this.timeout(120000) | ||
219 | |||
220 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.INTERNAL }) | ||
221 | await waitJobs([ server ]) | ||
222 | |||
223 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: null, videoFileToken: null }) | ||
224 | }) | ||
225 | |||
226 | it('Should not be able to access an internal video files without appropriate OAuth token and file token', async function () { | ||
227 | this.timeout(120000) | ||
228 | |||
229 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | ||
230 | await waitJobs([ server ]) | ||
231 | |||
232 | await checkVideoFiles({ | ||
233 | id: uuid, | ||
234 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
235 | token: userToken, | ||
236 | videoFileToken: unrelatedFileToken | ||
237 | }) | ||
238 | }) | ||
239 | |||
240 | it('Should be able to access a private video files with appropriate OAuth token or file token', async function () { | ||
241 | this.timeout(120000) | ||
242 | |||
243 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | ||
244 | const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) | ||
245 | |||
246 | await waitJobs([ server ]) | ||
247 | |||
248 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken }) | ||
249 | }) | ||
250 | |||
251 | it('Should be able to access a private video of another user with an admin OAuth token or file token', async function () { | ||
252 | this.timeout(120000) | ||
253 | |||
254 | const { uuid } = await server.videos.quickUpload({ name: 'video', token: userToken, privacy: VideoPrivacy.PRIVATE }) | ||
255 | const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) | ||
256 | |||
257 | await waitJobs([ server ]) | ||
258 | |||
259 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken }) | ||
260 | }) | ||
261 | }) | ||
262 | |||
263 | describe('Live static file path and check', function () { | ||
264 | let normalLiveId: string | ||
265 | let normalLive: LiveVideo | ||
266 | |||
267 | let permanentLiveId: string | ||
268 | let permanentLive: LiveVideo | ||
269 | |||
270 | let unrelatedFileToken: string | ||
271 | |||
272 | async function checkLiveFiles (live: LiveVideo, liveId: string) { | ||
273 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
274 | await server.live.waitUntilPublished({ videoId: liveId }) | ||
275 | |||
276 | const video = await server.videos.getWithToken({ id: liveId }) | ||
277 | const fileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) | ||
278 | |||
279 | const hls = video.streamingPlaylists[0] | ||
280 | |||
281 | for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) { | ||
282 | expectStartWith(url, server.url + '/static/streaming-playlists/hls/private/') | ||
283 | |||
284 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
285 | await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) | ||
286 | |||
287 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
288 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
289 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
290 | } | ||
291 | |||
292 | await stopFfmpeg(ffmpegCommand) | ||
293 | } | ||
294 | |||
295 | async function checkReplay (replay: VideoDetails) { | ||
296 | const fileToken = await server.videoToken.getVideoFileToken({ videoId: replay.uuid }) | ||
297 | |||
298 | const hls = replay.streamingPlaylists[0] | ||
299 | expect(hls.files).to.not.have.lengthOf(0) | ||
300 | |||
301 | for (const file of hls.files) { | ||
302 | await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
303 | await makeRawRequest({ url: file.fileUrl, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) | ||
304 | |||
305 | await makeRawRequest({ url: file.fileUrl, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
306 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
307 | await makeRawRequest({ | ||
308 | url: file.fileUrl, | ||
309 | query: { videoFileToken: unrelatedFileToken }, | ||
310 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
311 | }) | ||
312 | } | ||
313 | |||
314 | for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) { | ||
315 | expectStartWith(url, server.url + '/static/streaming-playlists/hls/private/') | ||
316 | |||
317 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
318 | await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) | ||
319 | |||
320 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
321 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
322 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
323 | } | ||
324 | } | ||
325 | |||
326 | before(async function () { | ||
327 | await server.config.enableMinimumTranscoding() | ||
328 | |||
329 | const { uuid } = await server.videos.quickUpload({ name: 'another video' }) | ||
330 | unrelatedFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) | ||
331 | |||
332 | await server.config.enableLive({ | ||
333 | allowReplay: true, | ||
334 | transcoding: true, | ||
335 | resolutions: 'min' | ||
336 | }) | ||
337 | |||
338 | { | ||
339 | const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: false, privacy: VideoPrivacy.PRIVATE }) | ||
340 | normalLiveId = video.uuid | ||
341 | normalLive = live | ||
342 | } | ||
343 | |||
344 | { | ||
345 | const { video, live } = await server.live.quickCreate({ saveReplay: true, permanentLive: true, privacy: VideoPrivacy.PRIVATE }) | ||
346 | permanentLiveId = video.uuid | ||
347 | permanentLive = live | ||
348 | } | ||
349 | }) | ||
350 | |||
351 | it('Should create a private normal live and have a private static path', async function () { | ||
352 | this.timeout(240000) | ||
353 | |||
354 | await checkLiveFiles(normalLive, normalLiveId) | ||
355 | }) | ||
356 | |||
357 | it('Should create a private permanent live and have a private static path', async function () { | ||
358 | this.timeout(240000) | ||
359 | |||
360 | await checkLiveFiles(permanentLive, permanentLiveId) | ||
361 | }) | ||
362 | |||
363 | it('Should have created a replay of the normal live with a private static path', async function () { | ||
364 | this.timeout(240000) | ||
365 | |||
366 | await server.live.waitUntilReplacedByReplay({ videoId: normalLiveId }) | ||
367 | |||
368 | const replay = await server.videos.getWithToken({ id: normalLiveId }) | ||
369 | await checkReplay(replay) | ||
370 | }) | ||
371 | |||
372 | it('Should have created a replay of the permanent live with a private static path', async function () { | ||
373 | this.timeout(240000) | ||
374 | |||
375 | await server.live.waitUntilWaiting({ videoId: permanentLiveId }) | ||
376 | await waitJobs([ server ]) | ||
377 | |||
378 | const live = await server.videos.getWithToken({ id: permanentLiveId }) | ||
379 | const replayFromList = await findExternalSavedVideo(server, live) | ||
380 | const replay = await server.videos.getWithToken({ id: replayFromList.id }) | ||
381 | |||
382 | await checkReplay(replay) | ||
383 | }) | ||
384 | }) | ||
385 | |||
386 | after(async function () { | ||
387 | await cleanupTests([ server ]) | ||
388 | }) | ||
389 | }) | ||
diff --git a/server/tests/cli/create-import-video-file-job.ts b/server/tests/cli/create-import-video-file-job.ts index 2cf2dd8f8..a4aa5f699 100644 --- a/server/tests/cli/create-import-video-file-job.ts +++ b/server/tests/cli/create-import-video-file-job.ts | |||
@@ -29,7 +29,7 @@ async function checkFiles (video: VideoDetails, objectStorage: boolean) { | |||
29 | for (const file of video.files) { | 29 | for (const file of video.files) { |
30 | if (objectStorage) expectStartWith(file.fileUrl, ObjectStorageCommand.getWebTorrentBaseUrl()) | 30 | if (objectStorage) expectStartWith(file.fileUrl, ObjectStorageCommand.getWebTorrentBaseUrl()) |
31 | 31 | ||
32 | await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) | 32 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) |
33 | } | 33 | } |
34 | } | 34 | } |
35 | 35 | ||
diff --git a/server/tests/cli/create-move-video-storage-job.ts b/server/tests/cli/create-move-video-storage-job.ts index 6a12a2c6c..ecdd75b76 100644 --- a/server/tests/cli/create-move-video-storage-job.ts +++ b/server/tests/cli/create-move-video-storage-job.ts | |||
@@ -22,7 +22,7 @@ async function checkFiles (origin: PeerTubeServer, video: VideoDetails, inObject | |||
22 | 22 | ||
23 | expectStartWith(file.fileUrl, start) | 23 | expectStartWith(file.fileUrl, start) |
24 | 24 | ||
25 | await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) | 25 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) |
26 | } | 26 | } |
27 | 27 | ||
28 | const start = inObjectStorage | 28 | const start = inObjectStorage |
@@ -36,7 +36,7 @@ async function checkFiles (origin: PeerTubeServer, video: VideoDetails, inObject | |||
36 | for (const file of hls.files) { | 36 | for (const file of hls.files) { |
37 | expectStartWith(file.fileUrl, start) | 37 | expectStartWith(file.fileUrl, start) |
38 | 38 | ||
39 | await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) | 39 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) |
40 | } | 40 | } |
41 | } | 41 | } |
42 | 42 | ||
diff --git a/server/tests/cli/create-transcoding-job.ts b/server/tests/cli/create-transcoding-job.ts index 8897d8c23..51bf04a80 100644 --- a/server/tests/cli/create-transcoding-job.ts +++ b/server/tests/cli/create-transcoding-job.ts | |||
@@ -23,7 +23,7 @@ async function checkFilesInObjectStorage (files: VideoFile[], type: 'webtorrent' | |||
23 | 23 | ||
24 | expectStartWith(file.fileUrl, shouldStartWith) | 24 | expectStartWith(file.fileUrl, shouldStartWith) |
25 | 25 | ||
26 | await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) | 26 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) |
27 | } | 27 | } |
28 | } | 28 | } |
29 | 29 | ||
diff --git a/server/tests/cli/prune-storage.ts b/server/tests/cli/prune-storage.ts index a89e17e3c..ba0fa1f86 100644 --- a/server/tests/cli/prune-storage.ts +++ b/server/tests/cli/prune-storage.ts | |||
@@ -5,7 +5,7 @@ import { createFile, readdir } from 'fs-extra' | |||
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import { wait } from '@shared/core-utils' | 6 | import { wait } from '@shared/core-utils' |
7 | import { buildUUID } from '@shared/extra-utils' | 7 | import { buildUUID } from '@shared/extra-utils' |
8 | import { HttpStatusCode, VideoPlaylistPrivacy } from '@shared/models' | 8 | import { HttpStatusCode, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' |
9 | import { | 9 | import { |
10 | cleanupTests, | 10 | cleanupTests, |
11 | CLICommand, | 11 | CLICommand, |
@@ -36,22 +36,28 @@ async function assertNotExists (server: PeerTubeServer, directory: string, subst | |||
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, 'videos') |
39 | expect(videosCount).to.equal(8) | 39 | expect(videosCount).to.equal(9) // 2 videos with 4 resolutions + private directory |
40 | |||
41 | const privateVideosCount = await countFiles(server, 'videos/private') | ||
42 | expect(privateVideosCount).to.equal(4) | ||
40 | 43 | ||
41 | const torrentsCount = await countFiles(server, 'torrents') | 44 | const torrentsCount = await countFiles(server, 'torrents') |
42 | expect(torrentsCount).to.equal(16) | 45 | expect(torrentsCount).to.equal(24) |
43 | 46 | ||
44 | const previewsCount = await countFiles(server, 'previews') | 47 | const previewsCount = await countFiles(server, 'previews') |
45 | expect(previewsCount).to.equal(2) | 48 | expect(previewsCount).to.equal(3) |
46 | 49 | ||
47 | const thumbnailsCount = await countFiles(server, 'thumbnails') | 50 | const thumbnailsCount = await countFiles(server, 'thumbnails') |
48 | expect(thumbnailsCount).to.equal(6) | 51 | expect(thumbnailsCount).to.equal(7) // 3 local videos, 1 local playlist, 2 remotes videos and 1 remote playlist |
49 | 52 | ||
50 | const avatarsCount = await countFiles(server, 'avatars') | 53 | const avatarsCount = await countFiles(server, 'avatars') |
51 | expect(avatarsCount).to.equal(4) | 54 | expect(avatarsCount).to.equal(4) |
52 | 55 | ||
53 | const hlsRootCount = await countFiles(server, 'streaming-playlists/hls') | 56 | const hlsRootCount = await countFiles(server, join('streaming-playlists', 'hls')) |
54 | expect(hlsRootCount).to.equal(2) | 57 | expect(hlsRootCount).to.equal(3) // 2 videos + private directory |
58 | |||
59 | const hlsPrivateRootCount = await countFiles(server, join('streaming-playlists', 'hls', 'private')) | ||
60 | expect(hlsPrivateRootCount).to.equal(1) | ||
55 | } | 61 | } |
56 | } | 62 | } |
57 | 63 | ||
@@ -67,8 +73,10 @@ describe('Test prune storage scripts', function () { | |||
67 | await setDefaultVideoChannel(servers) | 73 | await setDefaultVideoChannel(servers) |
68 | 74 | ||
69 | for (const server of servers) { | 75 | for (const server of servers) { |
70 | await server.videos.upload({ attributes: { name: 'video 1' } }) | 76 | await server.videos.upload({ attributes: { name: 'video 1', privacy: VideoPrivacy.PUBLIC } }) |
71 | await server.videos.upload({ attributes: { name: 'video 2' } }) | 77 | await server.videos.upload({ attributes: { name: 'video 2', privacy: VideoPrivacy.PUBLIC } }) |
78 | |||
79 | await server.videos.upload({ attributes: { name: 'video 3', privacy: VideoPrivacy.PRIVATE } }) | ||
72 | 80 | ||
73 | await server.users.updateMyAvatar({ fixture: 'avatar.png' }) | 81 | await server.users.updateMyAvatar({ fixture: 'avatar.png' }) |
74 | 82 | ||
@@ -123,13 +131,16 @@ describe('Test prune storage scripts', function () { | |||
123 | it('Should create some dirty files', async function () { | 131 | it('Should create some dirty files', async function () { |
124 | for (let i = 0; i < 2; i++) { | 132 | for (let i = 0; i < 2; i++) { |
125 | { | 133 | { |
126 | const base = servers[0].servers.buildDirectory('videos') | 134 | const basePublic = servers[0].servers.buildDirectory('videos') |
135 | const basePrivate = servers[0].servers.buildDirectory(join('videos', 'private')) | ||
127 | 136 | ||
128 | const n1 = buildUUID() + '.mp4' | 137 | const n1 = buildUUID() + '.mp4' |
129 | const n2 = buildUUID() + '.webm' | 138 | const n2 = buildUUID() + '.webm' |
130 | 139 | ||
131 | await createFile(join(base, n1)) | 140 | await createFile(join(basePublic, n1)) |
132 | await createFile(join(base, n2)) | 141 | await createFile(join(basePublic, n2)) |
142 | await createFile(join(basePrivate, n1)) | ||
143 | await createFile(join(basePrivate, n2)) | ||
133 | 144 | ||
134 | badNames['videos'] = [ n1, n2 ] | 145 | badNames['videos'] = [ n1, n2 ] |
135 | } | 146 | } |
@@ -184,10 +195,12 @@ describe('Test prune storage scripts', function () { | |||
184 | 195 | ||
185 | { | 196 | { |
186 | const directory = join('streaming-playlists', 'hls') | 197 | const directory = join('streaming-playlists', 'hls') |
187 | const base = servers[0].servers.buildDirectory(directory) | 198 | const basePublic = servers[0].servers.buildDirectory(directory) |
199 | const basePrivate = servers[0].servers.buildDirectory(join(directory, 'private')) | ||
188 | 200 | ||
189 | const n1 = buildUUID() | 201 | const n1 = buildUUID() |
190 | await createFile(join(base, n1)) | 202 | await createFile(join(basePublic, n1)) |
203 | await createFile(join(basePrivate, n1)) | ||
191 | badNames[directory] = [ n1 ] | 204 | badNames[directory] = [ n1 ] |
192 | } | 205 | } |
193 | } | 206 | } |
diff --git a/server/tests/cli/regenerate-thumbnails.ts b/server/tests/cli/regenerate-thumbnails.ts index f459b11b8..16a8adcda 100644 --- a/server/tests/cli/regenerate-thumbnails.ts +++ b/server/tests/cli/regenerate-thumbnails.ts | |||
@@ -6,7 +6,7 @@ import { | |||
6 | cleanupTests, | 6 | cleanupTests, |
7 | createMultipleServers, | 7 | createMultipleServers, |
8 | doubleFollow, | 8 | doubleFollow, |
9 | makeRawRequest, | 9 | makeGetRequest, |
10 | PeerTubeServer, | 10 | PeerTubeServer, |
11 | setAccessTokensToServers, | 11 | setAccessTokensToServers, |
12 | waitJobs | 12 | waitJobs |
@@ -16,8 +16,8 @@ async function testThumbnail (server: PeerTubeServer, videoId: number | string) | |||
16 | const video = await server.videos.get({ id: videoId }) | 16 | const video = await server.videos.get({ id: videoId }) |
17 | 17 | ||
18 | const requests = [ | 18 | const requests = [ |
19 | makeRawRequest(join(server.url, video.thumbnailPath), HttpStatusCode.OK_200), | 19 | makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }), |
20 | makeRawRequest(join(server.url, video.thumbnailPath), HttpStatusCode.OK_200) | 20 | makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) |
21 | ] | 21 | ] |
22 | 22 | ||
23 | for (const req of requests) { | 23 | for (const req of requests) { |
@@ -69,17 +69,17 @@ describe('Test regenerate thumbnails script', function () { | |||
69 | 69 | ||
70 | it('Should have empty thumbnails', async function () { | 70 | it('Should have empty thumbnails', async function () { |
71 | { | 71 | { |
72 | const res = await makeRawRequest(join(servers[0].url, video1.thumbnailPath), HttpStatusCode.OK_200) | 72 | const res = await makeGetRequest({ url: servers[0].url, path: video1.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) |
73 | expect(res.body).to.have.lengthOf(0) | 73 | expect(res.body).to.have.lengthOf(0) |
74 | } | 74 | } |
75 | 75 | ||
76 | { | 76 | { |
77 | const res = await makeRawRequest(join(servers[0].url, video2.thumbnailPath), HttpStatusCode.OK_200) | 77 | const res = await makeGetRequest({ url: servers[0].url, path: video2.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) |
78 | expect(res.body).to.not.have.lengthOf(0) | 78 | expect(res.body).to.not.have.lengthOf(0) |
79 | } | 79 | } |
80 | 80 | ||
81 | { | 81 | { |
82 | const res = await makeRawRequest(join(servers[0].url, remoteVideo.thumbnailPath), HttpStatusCode.OK_200) | 82 | const res = await makeGetRequest({ url: servers[0].url, path: remoteVideo.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) |
83 | expect(res.body).to.have.lengthOf(0) | 83 | expect(res.body).to.have.lengthOf(0) |
84 | } | 84 | } |
85 | }) | 85 | }) |
@@ -94,21 +94,21 @@ describe('Test regenerate thumbnails script', function () { | |||
94 | await testThumbnail(servers[0], video1.uuid) | 94 | await testThumbnail(servers[0], video1.uuid) |
95 | await testThumbnail(servers[0], video2.uuid) | 95 | await testThumbnail(servers[0], video2.uuid) |
96 | 96 | ||
97 | const res = await makeRawRequest(join(servers[0].url, remoteVideo.thumbnailPath), HttpStatusCode.OK_200) | 97 | const res = await makeGetRequest({ url: servers[0].url, path: remoteVideo.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) |
98 | expect(res.body).to.have.lengthOf(0) | 98 | expect(res.body).to.have.lengthOf(0) |
99 | }) | 99 | }) |
100 | 100 | ||
101 | it('Should have deleted old thumbnail files', async function () { | 101 | it('Should have deleted old thumbnail files', async function () { |
102 | { | 102 | { |
103 | await makeRawRequest(join(servers[0].url, video1.thumbnailPath), HttpStatusCode.NOT_FOUND_404) | 103 | await makeGetRequest({ url: servers[0].url, path: video1.thumbnailPath, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
104 | } | 104 | } |
105 | 105 | ||
106 | { | 106 | { |
107 | await makeRawRequest(join(servers[0].url, video2.thumbnailPath), HttpStatusCode.NOT_FOUND_404) | 107 | await makeGetRequest({ url: servers[0].url, path: video2.thumbnailPath, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
108 | } | 108 | } |
109 | 109 | ||
110 | { | 110 | { |
111 | const res = await makeRawRequest(join(servers[0].url, remoteVideo.thumbnailPath), HttpStatusCode.OK_200) | 111 | const res = await makeGetRequest({ url: servers[0].url, path: remoteVideo.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) |
112 | expect(res.body).to.have.lengthOf(0) | 112 | expect(res.body).to.have.lengthOf(0) |
113 | } | 113 | } |
114 | }) | 114 | }) |
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts index 0ddb641e6..c49175d5e 100644 --- a/server/tests/feeds/feeds.ts +++ b/server/tests/feeds/feeds.ts | |||
@@ -314,7 +314,7 @@ describe('Test syndication feeds', () => { | |||
314 | const jsonObj = JSON.parse(json) | 314 | const jsonObj = JSON.parse(json) |
315 | const imageUrl = jsonObj.icon | 315 | const imageUrl = jsonObj.icon |
316 | expect(imageUrl).to.include('/lazy-static/avatars/') | 316 | expect(imageUrl).to.include('/lazy-static/avatars/') |
317 | await makeRawRequest(imageUrl) | 317 | await makeRawRequest({ url: imageUrl }) |
318 | }) | 318 | }) |
319 | }) | 319 | }) |
320 | 320 | ||
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts index ae4b3cf5f..083fd43ca 100644 --- a/server/tests/plugins/filter-hooks.ts +++ b/server/tests/plugins/filter-hooks.ts | |||
@@ -6,6 +6,7 @@ import { | |||
6 | cleanupTests, | 6 | cleanupTests, |
7 | createMultipleServers, | 7 | createMultipleServers, |
8 | doubleFollow, | 8 | doubleFollow, |
9 | makeGetRequest, | ||
9 | makeRawRequest, | 10 | makeRawRequest, |
10 | PeerTubeServer, | 11 | PeerTubeServer, |
11 | PluginsCommand, | 12 | PluginsCommand, |
@@ -461,30 +462,41 @@ describe('Test plugin filter hooks', function () { | |||
461 | }) | 462 | }) |
462 | 463 | ||
463 | it('Should run filter:api.download.torrent.allowed.result', async function () { | 464 | it('Should run filter:api.download.torrent.allowed.result', async function () { |
464 | const res = await makeRawRequest(downloadVideos[0].files[0].torrentDownloadUrl, 403) | 465 | const res = await makeRawRequest({ url: downloadVideos[0].files[0].torrentDownloadUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
465 | expect(res.body.error).to.equal('Liu Bei') | 466 | expect(res.body.error).to.equal('Liu Bei') |
466 | 467 | ||
467 | await makeRawRequest(downloadVideos[1].files[0].torrentDownloadUrl, 200) | 468 | await makeRawRequest({ url: downloadVideos[1].files[0].torrentDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) |
468 | await makeRawRequest(downloadVideos[2].files[0].torrentDownloadUrl, 200) | 469 | await makeRawRequest({ url: downloadVideos[2].files[0].torrentDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) |
469 | }) | 470 | }) |
470 | 471 | ||
471 | it('Should run filter:api.download.video.allowed.result', async function () { | 472 | it('Should run filter:api.download.video.allowed.result', async function () { |
472 | { | 473 | { |
473 | const res = await makeRawRequest(downloadVideos[1].files[0].fileDownloadUrl, 403) | 474 | const res = await makeRawRequest({ url: downloadVideos[1].files[0].fileDownloadUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
474 | expect(res.body.error).to.equal('Cao Cao') | 475 | expect(res.body.error).to.equal('Cao Cao') |
475 | 476 | ||
476 | await makeRawRequest(downloadVideos[0].files[0].fileDownloadUrl, 200) | 477 | await makeRawRequest({ url: downloadVideos[0].files[0].fileDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) |
477 | await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200) | 478 | await makeRawRequest({ url: downloadVideos[2].files[0].fileDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) |
478 | } | 479 | } |
479 | 480 | ||
480 | { | 481 | { |
481 | const res = await makeRawRequest(downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl, 403) | 482 | const res = await makeRawRequest({ |
483 | url: downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl, | ||
484 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
485 | }) | ||
486 | |||
482 | expect(res.body.error).to.equal('Sun Jian') | 487 | expect(res.body.error).to.equal('Sun Jian') |
483 | 488 | ||
484 | await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200) | 489 | await makeRawRequest({ url: downloadVideos[2].files[0].fileDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) |
490 | |||
491 | await makeRawRequest({ | ||
492 | url: downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, | ||
493 | expectedStatus: HttpStatusCode.OK_200 | ||
494 | }) | ||
485 | 495 | ||
486 | await makeRawRequest(downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, 200) | 496 | await makeRawRequest({ |
487 | await makeRawRequest(downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl, 200) | 497 | url: downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl, |
498 | expectedStatus: HttpStatusCode.OK_200 | ||
499 | }) | ||
488 | } | 500 | } |
489 | }) | 501 | }) |
490 | }) | 502 | }) |
@@ -515,12 +527,12 @@ describe('Test plugin filter hooks', function () { | |||
515 | }) | 527 | }) |
516 | 528 | ||
517 | it('Should run filter:html.embed.video.allowed.result', async function () { | 529 | it('Should run filter:html.embed.video.allowed.result', async function () { |
518 | const res = await makeRawRequest(servers[0].url + embedVideos[0].embedPath, 200) | 530 | const res = await makeGetRequest({ url: servers[0].url, path: embedVideos[0].embedPath, expectedStatus: HttpStatusCode.OK_200 }) |
519 | expect(res.text).to.equal('Lu Bu') | 531 | expect(res.text).to.equal('Lu Bu') |
520 | }) | 532 | }) |
521 | 533 | ||
522 | it('Should run filter:html.embed.video-playlist.allowed.result', async function () { | 534 | it('Should run filter:html.embed.video-playlist.allowed.result', async function () { |
523 | const res = await makeRawRequest(servers[0].url + embedPlaylists[0].embedPath, 200) | 535 | const res = await makeGetRequest({ url: servers[0].url, path: embedPlaylists[0].embedPath, expectedStatus: HttpStatusCode.OK_200 }) |
524 | expect(res.text).to.equal('Diao Chan') | 536 | expect(res.text).to.equal('Diao Chan') |
525 | }) | 537 | }) |
526 | }) | 538 | }) |
diff --git a/server/tests/plugins/plugin-helpers.ts b/server/tests/plugins/plugin-helpers.ts index 31c18350a..f2bada4ee 100644 --- a/server/tests/plugins/plugin-helpers.ts +++ b/server/tests/plugins/plugin-helpers.ts | |||
@@ -307,7 +307,7 @@ describe('Test plugin helpers', function () { | |||
307 | expect(file.fps).to.equal(25) | 307 | expect(file.fps).to.equal(25) |
308 | 308 | ||
309 | expect(await pathExists(file.path)).to.be.true | 309 | expect(await pathExists(file.path)).to.be.true |
310 | await makeRawRequest(file.url, HttpStatusCode.OK_200) | 310 | await makeRawRequest({ url: file.url, expectedStatus: HttpStatusCode.OK_200 }) |
311 | } | 311 | } |
312 | } | 312 | } |
313 | 313 | ||
@@ -321,12 +321,12 @@ describe('Test plugin helpers', function () { | |||
321 | const miniature = body.thumbnails.find(t => t.type === ThumbnailType.MINIATURE) | 321 | const miniature = body.thumbnails.find(t => t.type === ThumbnailType.MINIATURE) |
322 | expect(miniature).to.exist | 322 | expect(miniature).to.exist |
323 | expect(await pathExists(miniature.path)).to.be.true | 323 | expect(await pathExists(miniature.path)).to.be.true |
324 | await makeRawRequest(miniature.url, HttpStatusCode.OK_200) | 324 | await makeRawRequest({ url: miniature.url, expectedStatus: HttpStatusCode.OK_200 }) |
325 | 325 | ||
326 | const preview = body.thumbnails.find(t => t.type === ThumbnailType.PREVIEW) | 326 | const preview = body.thumbnails.find(t => t.type === ThumbnailType.PREVIEW) |
327 | expect(preview).to.exist | 327 | expect(preview).to.exist |
328 | expect(await pathExists(preview.path)).to.be.true | 328 | expect(await pathExists(preview.path)).to.be.true |
329 | await makeRawRequest(preview.url, HttpStatusCode.OK_200) | 329 | await makeRawRequest({ url: preview.url, expectedStatus: HttpStatusCode.OK_200 }) |
330 | } | 330 | } |
331 | }) | 331 | }) |
332 | 332 | ||
diff --git a/server/tests/shared/streaming-playlists.ts b/server/tests/shared/streaming-playlists.ts index 74c25e99c..8ee04d921 100644 --- a/server/tests/shared/streaming-playlists.ts +++ b/server/tests/shared/streaming-playlists.ts | |||
@@ -1,9 +1,13 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
1 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
2 | import { basename } from 'path' | 4 | import { basename } from 'path' |
3 | import { removeFragmentedMP4Ext } from '@shared/core-utils' | 5 | import { removeFragmentedMP4Ext, uuidRegex } from '@shared/core-utils' |
4 | import { sha256 } from '@shared/extra-utils' | 6 | import { sha256 } from '@shared/extra-utils' |
5 | import { HttpStatusCode, VideoStreamingPlaylist } from '@shared/models' | 7 | import { HttpStatusCode, VideoStreamingPlaylist, VideoStreamingPlaylistType } from '@shared/models' |
6 | import { PeerTubeServer } from '@shared/server-commands' | 8 | import { makeRawRequest, PeerTubeServer, webtorrentAdd } from '@shared/server-commands' |
9 | import { expectStartWith } from './checks' | ||
10 | import { hlsInfohashExist } from './tracker' | ||
7 | 11 | ||
8 | async function checkSegmentHash (options: { | 12 | async function checkSegmentHash (options: { |
9 | server: PeerTubeServer | 13 | server: PeerTubeServer |
@@ -75,8 +79,118 @@ async function checkResolutionsInMasterPlaylist (options: { | |||
75 | expect(playlistsLength).to.have.lengthOf(resolutions.length) | 79 | expect(playlistsLength).to.have.lengthOf(resolutions.length) |
76 | } | 80 | } |
77 | 81 | ||
82 | async function completeCheckHlsPlaylist (options: { | ||
83 | servers: PeerTubeServer[] | ||
84 | videoUUID: string | ||
85 | hlsOnly: boolean | ||
86 | |||
87 | resolutions?: number[] | ||
88 | objectStorageBaseUrl: string | ||
89 | }) { | ||
90 | const { videoUUID, hlsOnly, objectStorageBaseUrl } = options | ||
91 | |||
92 | const resolutions = options.resolutions ?? [ 240, 360, 480, 720 ] | ||
93 | |||
94 | for (const server of options.servers) { | ||
95 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
96 | const baseUrl = `http://${videoDetails.account.host}` | ||
97 | |||
98 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
99 | |||
100 | const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
101 | expect(hlsPlaylist).to.not.be.undefined | ||
102 | |||
103 | const hlsFiles = hlsPlaylist.files | ||
104 | expect(hlsFiles).to.have.lengthOf(resolutions.length) | ||
105 | |||
106 | if (hlsOnly) expect(videoDetails.files).to.have.lengthOf(0) | ||
107 | else expect(videoDetails.files).to.have.lengthOf(resolutions.length) | ||
108 | |||
109 | // Check JSON files | ||
110 | for (const resolution of resolutions) { | ||
111 | const file = hlsFiles.find(f => f.resolution.id === resolution) | ||
112 | expect(file).to.not.be.undefined | ||
113 | |||
114 | expect(file.magnetUri).to.have.lengthOf.above(2) | ||
115 | expect(file.torrentUrl).to.match( | ||
116 | new RegExp(`http://${server.host}/lazy-static/torrents/${uuidRegex}-${file.resolution.id}-hls.torrent`) | ||
117 | ) | ||
118 | |||
119 | if (objectStorageBaseUrl) { | ||
120 | expectStartWith(file.fileUrl, objectStorageBaseUrl) | ||
121 | } else { | ||
122 | expect(file.fileUrl).to.match( | ||
123 | new RegExp(`${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${uuidRegex}-${file.resolution.id}-fragmented.mp4`) | ||
124 | ) | ||
125 | } | ||
126 | |||
127 | expect(file.resolution.label).to.equal(resolution + 'p') | ||
128 | |||
129 | await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
130 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
131 | |||
132 | const torrent = await webtorrentAdd(file.magnetUri, true) | ||
133 | expect(torrent.files).to.be.an('array') | ||
134 | expect(torrent.files.length).to.equal(1) | ||
135 | expect(torrent.files[0].path).to.exist.and.to.not.equal('') | ||
136 | } | ||
137 | |||
138 | // Check master playlist | ||
139 | { | ||
140 | await checkResolutionsInMasterPlaylist({ server, playlistUrl: hlsPlaylist.playlistUrl, resolutions }) | ||
141 | |||
142 | const masterPlaylist = await server.streamingPlaylists.get({ url: hlsPlaylist.playlistUrl }) | ||
143 | |||
144 | let i = 0 | ||
145 | for (const resolution of resolutions) { | ||
146 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | ||
147 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | ||
148 | |||
149 | const url = 'http://' + videoDetails.account.host | ||
150 | await hlsInfohashExist(url, hlsPlaylist.playlistUrl, i) | ||
151 | |||
152 | i++ | ||
153 | } | ||
154 | } | ||
155 | |||
156 | // Check resolution playlists | ||
157 | { | ||
158 | for (const resolution of resolutions) { | ||
159 | const file = hlsFiles.find(f => f.resolution.id === resolution) | ||
160 | const playlistName = removeFragmentedMP4Ext(basename(file.fileUrl)) + '.m3u8' | ||
161 | |||
162 | const url = objectStorageBaseUrl | ||
163 | ? `${objectStorageBaseUrl}hls/${videoUUID}/${playlistName}` | ||
164 | : `${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${playlistName}` | ||
165 | |||
166 | const subPlaylist = await server.streamingPlaylists.get({ url }) | ||
167 | |||
168 | expect(subPlaylist).to.match(new RegExp(`${uuidRegex}-${resolution}-fragmented.mp4`)) | ||
169 | expect(subPlaylist).to.contain(basename(file.fileUrl)) | ||
170 | } | ||
171 | } | ||
172 | |||
173 | { | ||
174 | const baseUrlAndPath = objectStorageBaseUrl | ||
175 | ? objectStorageBaseUrl + 'hls/' + videoUUID | ||
176 | : baseUrl + '/static/streaming-playlists/hls/' + videoUUID | ||
177 | |||
178 | for (const resolution of resolutions) { | ||
179 | await checkSegmentHash({ | ||
180 | server, | ||
181 | baseUrlPlaylist: baseUrlAndPath, | ||
182 | baseUrlSegment: baseUrlAndPath, | ||
183 | resolution, | ||
184 | hlsPlaylist | ||
185 | }) | ||
186 | } | ||
187 | } | ||
188 | } | ||
189 | } | ||
190 | |||
78 | export { | 191 | export { |
79 | checkSegmentHash, | 192 | checkSegmentHash, |
80 | checkLiveSegmentHash, | 193 | checkLiveSegmentHash, |
81 | checkResolutionsInMasterPlaylist | 194 | checkResolutionsInMasterPlaylist, |
195 | completeCheckHlsPlaylist | ||
82 | } | 196 | } |
diff --git a/server/tests/shared/videos.ts b/server/tests/shared/videos.ts index e18329e07..c8339584b 100644 --- a/server/tests/shared/videos.ts +++ b/server/tests/shared/videos.ts | |||
@@ -125,9 +125,9 @@ async function completeVideoCheck ( | |||
125 | expect(file.fileDownloadUrl).to.match(new RegExp(`http://${originHost}/download/videos/${uuidRegex}-${file.resolution.id}${extension}`)) | 125 | expect(file.fileDownloadUrl).to.match(new RegExp(`http://${originHost}/download/videos/${uuidRegex}-${file.resolution.id}${extension}`)) |
126 | 126 | ||
127 | await Promise.all([ | 127 | await Promise.all([ |
128 | makeRawRequest(file.torrentUrl, 200), | 128 | makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 }), |
129 | makeRawRequest(file.torrentDownloadUrl, 200), | 129 | makeRawRequest({ url: file.torrentDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }), |
130 | makeRawRequest(file.metadataUrl, 200) | 130 | makeRawRequest({ url: file.metadataUrl, expectedStatus: HttpStatusCode.OK_200 }) |
131 | ]) | 131 | ]) |
132 | 132 | ||
133 | expect(file.resolution.id).to.equal(attributeFile.resolution) | 133 | expect(file.resolution.id).to.equal(attributeFile.resolution) |
diff --git a/shared/core-utils/common/url.ts b/shared/core-utils/common/url.ts index fd54e7594..d1c399f7b 100644 --- a/shared/core-utils/common/url.ts +++ b/shared/core-utils/common/url.ts | |||
@@ -1,6 +1,16 @@ | |||
1 | import { Video, VideoPlaylist } from '../../models' | 1 | import { Video, VideoPlaylist } from '../../models' |
2 | import { secondsToTime } from './date' | 2 | import { secondsToTime } from './date' |
3 | 3 | ||
4 | function addQueryParams (url: string, params: { [ id: string ]: string }) { | ||
5 | const objUrl = new URL(url) | ||
6 | |||
7 | for (const key of Object.keys(params)) { | ||
8 | objUrl.searchParams.append(key, params[key]) | ||
9 | } | ||
10 | |||
11 | return objUrl.toString() | ||
12 | } | ||
13 | |||
4 | function buildPlaylistLink (playlist: Pick<VideoPlaylist, 'shortUUID'>, base?: string) { | 14 | function buildPlaylistLink (playlist: Pick<VideoPlaylist, 'shortUUID'>, base?: string) { |
5 | return (base ?? window.location.origin) + buildPlaylistWatchPath(playlist) | 15 | return (base ?? window.location.origin) + buildPlaylistWatchPath(playlist) |
6 | } | 16 | } |
@@ -103,6 +113,8 @@ function decoratePlaylistLink (options: { | |||
103 | // --------------------------------------------------------------------------- | 113 | // --------------------------------------------------------------------------- |
104 | 114 | ||
105 | export { | 115 | export { |
116 | addQueryParams, | ||
117 | |||
106 | buildPlaylistLink, | 118 | buildPlaylistLink, |
107 | buildVideoLink, | 119 | buildVideoLink, |
108 | 120 | ||
diff --git a/shared/models/server/debug.model.ts b/shared/models/server/debug.model.ts index 1c4597b8b..41f2109af 100644 --- a/shared/models/server/debug.model.ts +++ b/shared/models/server/debug.model.ts | |||
@@ -8,4 +8,5 @@ export interface SendDebugCommand { | |||
8 | | 'process-video-views-buffer' | 8 | | 'process-video-views-buffer' |
9 | | 'process-video-viewers' | 9 | | 'process-video-viewers' |
10 | | 'process-video-channel-sync-latest' | 10 | | 'process-video-channel-sync-latest' |
11 | | 'process-update-videos-scheduler' | ||
11 | } | 12 | } |
diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index f8e6976d3..4c1790228 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts | |||
@@ -33,6 +33,8 @@ export * from './video-storage.enum' | |||
33 | export * from './video-streaming-playlist.model' | 33 | export * from './video-streaming-playlist.model' |
34 | export * from './video-streaming-playlist.type' | 34 | export * from './video-streaming-playlist.type' |
35 | 35 | ||
36 | export * from './video-token.model' | ||
37 | |||
36 | export * from './video-update.model' | 38 | export * from './video-update.model' |
37 | export * from './video-view.model' | 39 | export * from './video-view.model' |
38 | export * from './video.model' | 40 | export * from './video.model' |
diff --git a/shared/models/videos/video-token.model.ts b/shared/models/videos/video-token.model.ts new file mode 100644 index 000000000..aefea565f --- /dev/null +++ b/shared/models/videos/video-token.model.ts | |||
@@ -0,0 +1,6 @@ | |||
1 | export interface VideoToken { | ||
2 | files: { | ||
3 | token: string | ||
4 | expires: string | Date | ||
5 | } | ||
6 | } | ||
diff --git a/shared/server-commands/requests/requests.ts b/shared/server-commands/requests/requests.ts index 8cc1245e0..b247017fd 100644 --- a/shared/server-commands/requests/requests.ts +++ b/shared/server-commands/requests/requests.ts | |||
@@ -3,7 +3,7 @@ | |||
3 | import { decode } from 'querystring' | 3 | import { decode } from 'querystring' |
4 | import request from 'supertest' | 4 | import request from 'supertest' |
5 | import { URL } from 'url' | 5 | import { URL } from 'url' |
6 | import { buildAbsoluteFixturePath } from '@shared/core-utils' | 6 | import { buildAbsoluteFixturePath, pick } from '@shared/core-utils' |
7 | import { HttpStatusCode } from '@shared/models' | 7 | import { HttpStatusCode } from '@shared/models' |
8 | 8 | ||
9 | export type CommonRequestParams = { | 9 | export type CommonRequestParams = { |
@@ -21,10 +21,21 @@ export type CommonRequestParams = { | |||
21 | expectedStatus?: HttpStatusCode | 21 | expectedStatus?: HttpStatusCode |
22 | } | 22 | } |
23 | 23 | ||
24 | function makeRawRequest (url: string, expectedStatus?: HttpStatusCode, range?: string) { | 24 | function makeRawRequest (options: { |
25 | const { host, protocol, pathname } = new URL(url) | 25 | url: string |
26 | token?: string | ||
27 | expectedStatus?: HttpStatusCode | ||
28 | range?: string | ||
29 | query?: { [ id: string ]: string } | ||
30 | }) { | ||
31 | const { host, protocol, pathname } = new URL(options.url) | ||
32 | |||
33 | return makeGetRequest({ | ||
34 | url: `${protocol}//${host}`, | ||
35 | path: pathname, | ||
26 | 36 | ||
27 | return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, expectedStatus, range }) | 37 | ...pick(options, [ 'expectedStatus', 'range', 'token', 'query' ]) |
38 | }) | ||
28 | } | 39 | } |
29 | 40 | ||
30 | function makeGetRequest (options: CommonRequestParams & { | 41 | function makeGetRequest (options: CommonRequestParams & { |
diff --git a/shared/server-commands/server/server.ts b/shared/server-commands/server/server.ts index 7096faf21..c062e6986 100644 --- a/shared/server-commands/server/server.ts +++ b/shared/server-commands/server/server.ts | |||
@@ -36,6 +36,7 @@ import { | |||
36 | StreamingPlaylistsCommand, | 36 | StreamingPlaylistsCommand, |
37 | VideosCommand, | 37 | VideosCommand, |
38 | VideoStudioCommand, | 38 | VideoStudioCommand, |
39 | VideoTokenCommand, | ||
39 | ViewsCommand | 40 | ViewsCommand |
40 | } from '../videos' | 41 | } from '../videos' |
41 | import { CommentsCommand } from '../videos/comments-command' | 42 | import { CommentsCommand } from '../videos/comments-command' |
@@ -145,6 +146,7 @@ export class PeerTubeServer { | |||
145 | videoStats?: VideoStatsCommand | 146 | videoStats?: VideoStatsCommand |
146 | views?: ViewsCommand | 147 | views?: ViewsCommand |
147 | twoFactor?: TwoFactorCommand | 148 | twoFactor?: TwoFactorCommand |
149 | videoToken?: VideoTokenCommand | ||
148 | 150 | ||
149 | constructor (options: { serverNumber: number } | { url: string }) { | 151 | constructor (options: { serverNumber: number } | { url: string }) { |
150 | if ((options as any).url) { | 152 | if ((options as any).url) { |
@@ -427,5 +429,6 @@ export class PeerTubeServer { | |||
427 | this.videoStats = new VideoStatsCommand(this) | 429 | this.videoStats = new VideoStatsCommand(this) |
428 | this.views = new ViewsCommand(this) | 430 | this.views = new ViewsCommand(this) |
429 | this.twoFactor = new TwoFactorCommand(this) | 431 | this.twoFactor = new TwoFactorCommand(this) |
432 | this.videoToken = new VideoTokenCommand(this) | ||
430 | } | 433 | } |
431 | } | 434 | } |
diff --git a/shared/server-commands/videos/index.ts b/shared/server-commands/videos/index.ts index b4d6fa37b..c17f6ef20 100644 --- a/shared/server-commands/videos/index.ts +++ b/shared/server-commands/videos/index.ts | |||
@@ -14,5 +14,6 @@ export * from './services-command' | |||
14 | export * from './streaming-playlists-command' | 14 | export * from './streaming-playlists-command' |
15 | export * from './comments-command' | 15 | export * from './comments-command' |
16 | export * from './video-studio-command' | 16 | export * from './video-studio-command' |
17 | export * from './video-token-command' | ||
17 | export * from './views-command' | 18 | export * from './views-command' |
18 | export * from './videos-command' | 19 | export * from './videos-command' |
diff --git a/shared/server-commands/videos/live-command.ts b/shared/server-commands/videos/live-command.ts index b163f7189..de193fa49 100644 --- a/shared/server-commands/videos/live-command.ts +++ b/shared/server-commands/videos/live-command.ts | |||
@@ -12,6 +12,7 @@ import { | |||
12 | ResultList, | 12 | ResultList, |
13 | VideoCreateResult, | 13 | VideoCreateResult, |
14 | VideoDetails, | 14 | VideoDetails, |
15 | VideoPrivacy, | ||
15 | VideoState | 16 | VideoState |
16 | } from '@shared/models' | 17 | } from '@shared/models' |
17 | import { unwrapBody } from '../requests' | 18 | import { unwrapBody } from '../requests' |
@@ -115,6 +116,31 @@ export class LiveCommand extends AbstractCommand { | |||
115 | return body.video | 116 | return body.video |
116 | } | 117 | } |
117 | 118 | ||
119 | async quickCreate (options: OverrideCommandOptions & { | ||
120 | saveReplay: boolean | ||
121 | permanentLive: boolean | ||
122 | privacy?: VideoPrivacy | ||
123 | }) { | ||
124 | const { saveReplay, permanentLive, privacy } = options | ||
125 | |||
126 | const { uuid } = await this.create({ | ||
127 | ...options, | ||
128 | |||
129 | fields: { | ||
130 | name: 'live', | ||
131 | permanentLive, | ||
132 | saveReplay, | ||
133 | channelId: this.server.store.channel.id, | ||
134 | privacy | ||
135 | } | ||
136 | }) | ||
137 | |||
138 | const video = await this.server.videos.getWithToken({ id: uuid }) | ||
139 | const live = await this.get({ videoId: uuid }) | ||
140 | |||
141 | return { video, live } | ||
142 | } | ||
143 | |||
118 | // --------------------------------------------------------------------------- | 144 | // --------------------------------------------------------------------------- |
119 | 145 | ||
120 | async sendRTMPStreamInVideo (options: OverrideCommandOptions & { | 146 | async sendRTMPStreamInVideo (options: OverrideCommandOptions & { |
diff --git a/shared/server-commands/videos/live.ts b/shared/server-commands/videos/live.ts index 0d9c32aab..ee7444b64 100644 --- a/shared/server-commands/videos/live.ts +++ b/shared/server-commands/videos/live.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import ffmpeg, { FfmpegCommand } from 'fluent-ffmpeg' | 1 | import ffmpeg, { FfmpegCommand } from 'fluent-ffmpeg' |
2 | import { buildAbsoluteFixturePath, wait } from '@shared/core-utils' | 2 | import { buildAbsoluteFixturePath, wait } from '@shared/core-utils' |
3 | import { VideoDetails, VideoInclude } from '@shared/models' | 3 | import { VideoDetails, VideoInclude, VideoPrivacy } from '@shared/models' |
4 | import { PeerTubeServer } from '../server/server' | 4 | import { PeerTubeServer } from '../server/server' |
5 | 5 | ||
6 | function sendRTMPStream (options: { | 6 | function sendRTMPStream (options: { |
@@ -98,7 +98,10 @@ async function waitUntilLiveReplacedByReplayOnAllServers (servers: PeerTubeServe | |||
98 | } | 98 | } |
99 | 99 | ||
100 | async function findExternalSavedVideo (server: PeerTubeServer, liveDetails: VideoDetails) { | 100 | async function findExternalSavedVideo (server: PeerTubeServer, liveDetails: VideoDetails) { |
101 | const { data } = await server.videos.list({ token: server.accessToken, sort: '-publishedAt', include: VideoInclude.BLACKLISTED }) | 101 | const include = VideoInclude.BLACKLISTED |
102 | const privacyOneOf = [ VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.PUBLIC, VideoPrivacy.UNLISTED ] | ||
103 | |||
104 | const { data } = await server.videos.list({ token: server.accessToken, sort: '-publishedAt', include, privacyOneOf }) | ||
102 | 105 | ||
103 | return data.find(v => v.name === liveDetails.name + ' - ' + new Date(liveDetails.publishedAt).toLocaleString()) | 106 | return data.find(v => v.name === liveDetails.name + ' - ' + new Date(liveDetails.publishedAt).toLocaleString()) |
104 | } | 107 | } |
diff --git a/shared/server-commands/videos/video-token-command.ts b/shared/server-commands/videos/video-token-command.ts new file mode 100644 index 000000000..0531bee65 --- /dev/null +++ b/shared/server-commands/videos/video-token-command.ts | |||
@@ -0,0 +1,31 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ | ||
2 | |||
3 | import { HttpStatusCode, VideoToken } from '@shared/models' | ||
4 | import { unwrapBody } from '../requests' | ||
5 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
6 | |||
7 | export class VideoTokenCommand extends AbstractCommand { | ||
8 | |||
9 | create (options: OverrideCommandOptions & { | ||
10 | videoId: number | string | ||
11 | }) { | ||
12 | const { videoId } = options | ||
13 | const path = '/api/v1/videos/' + videoId + '/token' | ||
14 | |||
15 | return unwrapBody<VideoToken>(this.postBodyRequest({ | ||
16 | ...options, | ||
17 | |||
18 | path, | ||
19 | implicitToken: true, | ||
20 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
21 | })) | ||
22 | } | ||
23 | |||
24 | async getVideoFileToken (options: OverrideCommandOptions & { | ||
25 | videoId: number | string | ||
26 | }) { | ||
27 | const { files } = await this.create(options) | ||
28 | |||
29 | return files.token | ||
30 | } | ||
31 | } | ||
diff --git a/shared/server-commands/videos/videos-command.ts b/shared/server-commands/videos/videos-command.ts index 168391523..5ec3b6ba8 100644 --- a/shared/server-commands/videos/videos-command.ts +++ b/shared/server-commands/videos/videos-command.ts | |||
@@ -342,8 +342,9 @@ export class VideosCommand extends AbstractCommand { | |||
342 | async upload (options: OverrideCommandOptions & { | 342 | async upload (options: OverrideCommandOptions & { |
343 | attributes?: VideoEdit | 343 | attributes?: VideoEdit |
344 | mode?: 'legacy' | 'resumable' // default legacy | 344 | mode?: 'legacy' | 'resumable' // default legacy |
345 | waitTorrentGeneration?: boolean // default true | ||
345 | } = {}) { | 346 | } = {}) { |
346 | const { mode = 'legacy' } = options | 347 | const { mode = 'legacy', waitTorrentGeneration } = options |
347 | let defaultChannelId = 1 | 348 | let defaultChannelId = 1 |
348 | 349 | ||
349 | try { | 350 | try { |
@@ -377,7 +378,7 @@ export class VideosCommand extends AbstractCommand { | |||
377 | 378 | ||
378 | // Wait torrent generation | 379 | // Wait torrent generation |
379 | const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 }) | 380 | const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 }) |
380 | if (expectedStatus === HttpStatusCode.OK_200) { | 381 | if (expectedStatus === HttpStatusCode.OK_200 && waitTorrentGeneration) { |
381 | let video: VideoDetails | 382 | let video: VideoDetails |
382 | 383 | ||
383 | do { | 384 | do { |
@@ -692,6 +693,7 @@ export class VideosCommand extends AbstractCommand { | |||
692 | 'categoryOneOf', | 693 | 'categoryOneOf', |
693 | 'licenceOneOf', | 694 | 'licenceOneOf', |
694 | 'languageOneOf', | 695 | 'languageOneOf', |
696 | 'privacyOneOf', | ||
695 | 'tagsOneOf', | 697 | 'tagsOneOf', |
696 | 'tagsAllOf', | 698 | 'tagsAllOf', |
697 | 'isLocal', | 699 | 'isLocal', |
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 2fb154dbd..7ffe8c67b 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml | |||
@@ -276,8 +276,8 @@ tags: | |||
276 | description: Video transcoding related operations | 276 | description: Video transcoding related operations |
277 | - name: Video stats | 277 | - name: Video stats |
278 | description: Video statistics | 278 | description: Video statistics |
279 | - name: Feeds | 279 | - name: Video Feeds |
280 | description: Server syndication feeds | 280 | description: Server syndication feeds of videos |
281 | - name: Search | 281 | - name: Search |
282 | description: | | 282 | description: | |
283 | The search helps to find _videos_ or _channels_ from within the instance and beyond. | 283 | The search helps to find _videos_ or _channels_ from within the instance and beyond. |
@@ -299,6 +299,12 @@ tags: | |||
299 | Statistics | 299 | Statistics |
300 | 300 | ||
301 | x-tagGroups: | 301 | x-tagGroups: |
302 | - name: Static endpoints | ||
303 | tags: | ||
304 | - Static Video Files | ||
305 | - name: Feeds | ||
306 | tags: | ||
307 | - Video Feeds | ||
302 | - name: Auth | 308 | - name: Auth |
303 | tags: | 309 | tags: |
304 | - Register | 310 | - Register |
@@ -327,7 +333,6 @@ x-tagGroups: | |||
327 | - Video Files | 333 | - Video Files |
328 | - Video Transcoding | 334 | - Video Transcoding |
329 | - Live Videos | 335 | - Live Videos |
330 | - Feeds | ||
331 | - Channels Sync | 336 | - Channels Sync |
332 | - name: Search | 337 | - name: Search |
333 | tags: | 338 | tags: |
@@ -349,7 +354,326 @@ x-tagGroups: | |||
349 | - Logs | 354 | - Logs |
350 | - Job | 355 | - Job |
351 | paths: | 356 | paths: |
352 | '/accounts/{name}': | 357 | '/static/webseed/{filename}': |
358 | get: | ||
359 | tags: | ||
360 | - Static Video Files | ||
361 | summary: Get public WebTorrent video file | ||
362 | parameters: | ||
363 | - $ref: '#/components/parameters/staticFilename' | ||
364 | responses: | ||
365 | '200': | ||
366 | description: successful operation | ||
367 | '404': | ||
368 | description: not found | ||
369 | '/static/webseed/private/{filename}': | ||
370 | get: | ||
371 | tags: | ||
372 | - Static Video Files | ||
373 | summary: Get private WebTorrent video file | ||
374 | parameters: | ||
375 | - $ref: '#/components/parameters/staticFilename' | ||
376 | - $ref: '#/components/parameters/videoFileToken' | ||
377 | security: | ||
378 | - OAuth2: [] | ||
379 | responses: | ||
380 | '200': | ||
381 | description: successful operation | ||
382 | '403': | ||
383 | description: invalid auth | ||
384 | '404': | ||
385 | description: not found | ||
386 | |||
387 | '/static/streaming-playlists/hls/{filename}': | ||
388 | get: | ||
389 | tags: | ||
390 | - Static Video Files | ||
391 | summary: Get public HLS video file | ||
392 | parameters: | ||
393 | - $ref: '#/components/parameters/staticFilename' | ||
394 | security: | ||
395 | - OAuth2: [] | ||
396 | responses: | ||
397 | '200': | ||
398 | description: successful operation | ||
399 | '403': | ||
400 | description: invalid auth | ||
401 | '404': | ||
402 | description: not found | ||
403 | '/static/streaming-playlists/hls/private/{filename}': | ||
404 | get: | ||
405 | tags: | ||
406 | - Static Video Files | ||
407 | summary: Get private HLS video file | ||
408 | parameters: | ||
409 | - $ref: '#/components/parameters/staticFilename' | ||
410 | - $ref: '#/components/parameters/videoFileToken' | ||
411 | security: | ||
412 | - OAuth2: [] | ||
413 | responses: | ||
414 | '200': | ||
415 | description: successful operation | ||
416 | '403': | ||
417 | description: invalid auth | ||
418 | '404': | ||
419 | description: not found | ||
420 | |||
421 | |||
422 | '/feeds/video-comments.{format}': | ||
423 | get: | ||
424 | tags: | ||
425 | - Video Feeds | ||
426 | summary: List comments on videos | ||
427 | operationId: getSyndicatedComments | ||
428 | parameters: | ||
429 | - name: format | ||
430 | in: path | ||
431 | required: true | ||
432 | description: 'format expected (we focus on making `rss` the most featureful ; it serves [Media RSS](https://www.rssboard.org/media-rss))' | ||
433 | schema: | ||
434 | type: string | ||
435 | enum: | ||
436 | - xml | ||
437 | - rss | ||
438 | - rss2 | ||
439 | - atom | ||
440 | - atom1 | ||
441 | - json | ||
442 | - json1 | ||
443 | - name: videoId | ||
444 | in: query | ||
445 | description: 'limit listing to a specific video' | ||
446 | schema: | ||
447 | type: string | ||
448 | - name: accountId | ||
449 | in: query | ||
450 | description: 'limit listing to a specific account' | ||
451 | schema: | ||
452 | type: string | ||
453 | - name: accountName | ||
454 | in: query | ||
455 | description: 'limit listing to a specific account' | ||
456 | schema: | ||
457 | type: string | ||
458 | - name: videoChannelId | ||
459 | in: query | ||
460 | description: 'limit listing to a specific video channel' | ||
461 | schema: | ||
462 | type: string | ||
463 | - name: videoChannelName | ||
464 | in: query | ||
465 | description: 'limit listing to a specific video channel' | ||
466 | schema: | ||
467 | type: string | ||
468 | responses: | ||
469 | '204': | ||
470 | description: successful operation | ||
471 | headers: | ||
472 | Cache-Control: | ||
473 | schema: | ||
474 | type: string | ||
475 | default: 'max-age=900' # 15 min cache | ||
476 | content: | ||
477 | application/xml: | ||
478 | schema: | ||
479 | $ref: '#/components/schemas/VideoCommentsForXML' | ||
480 | examples: | ||
481 | nightly: | ||
482 | externalValue: https://peertube2.cpy.re/feeds/video-comments.xml?filter=local | ||
483 | application/rss+xml: | ||
484 | schema: | ||
485 | $ref: '#/components/schemas/VideoCommentsForXML' | ||
486 | examples: | ||
487 | nightly: | ||
488 | externalValue: https://peertube2.cpy.re/feeds/video-comments.rss?filter=local | ||
489 | text/xml: | ||
490 | schema: | ||
491 | $ref: '#/components/schemas/VideoCommentsForXML' | ||
492 | examples: | ||
493 | nightly: | ||
494 | externalValue: https://peertube2.cpy.re/feeds/video-comments.xml?filter=local | ||
495 | application/atom+xml: | ||
496 | schema: | ||
497 | $ref: '#/components/schemas/VideoCommentsForXML' | ||
498 | examples: | ||
499 | nightly: | ||
500 | externalValue: https://peertube2.cpy.re/feeds/video-comments.atom?filter=local | ||
501 | application/json: | ||
502 | schema: | ||
503 | type: object | ||
504 | examples: | ||
505 | nightly: | ||
506 | externalValue: https://peertube2.cpy.re/feeds/video-comments.json?filter=local | ||
507 | '400': | ||
508 | x-summary: field inconsistencies | ||
509 | description: > | ||
510 | Arises when: | ||
511 | - videoId filter is mixed with a channel filter | ||
512 | '404': | ||
513 | description: video, video channel or account not found | ||
514 | '406': | ||
515 | description: accept header unsupported | ||
516 | |||
517 | '/feeds/videos.{format}': | ||
518 | get: | ||
519 | tags: | ||
520 | - Video Feeds | ||
521 | summary: List videos | ||
522 | operationId: getSyndicatedVideos | ||
523 | parameters: | ||
524 | - name: format | ||
525 | in: path | ||
526 | required: true | ||
527 | description: 'format expected (we focus on making `rss` the most featureful ; it serves [Media RSS](https://www.rssboard.org/media-rss))' | ||
528 | schema: | ||
529 | type: string | ||
530 | enum: | ||
531 | - xml | ||
532 | - rss | ||
533 | - rss2 | ||
534 | - atom | ||
535 | - atom1 | ||
536 | - json | ||
537 | - json1 | ||
538 | - name: accountId | ||
539 | in: query | ||
540 | description: 'limit listing to a specific account' | ||
541 | schema: | ||
542 | type: string | ||
543 | - name: accountName | ||
544 | in: query | ||
545 | description: 'limit listing to a specific account' | ||
546 | schema: | ||
547 | type: string | ||
548 | - name: videoChannelId | ||
549 | in: query | ||
550 | description: 'limit listing to a specific video channel' | ||
551 | schema: | ||
552 | type: string | ||
553 | - name: videoChannelName | ||
554 | in: query | ||
555 | description: 'limit listing to a specific video channel' | ||
556 | schema: | ||
557 | type: string | ||
558 | - $ref: '#/components/parameters/sort' | ||
559 | - $ref: '#/components/parameters/nsfw' | ||
560 | - $ref: '#/components/parameters/isLocal' | ||
561 | - $ref: '#/components/parameters/include' | ||
562 | - $ref: '#/components/parameters/privacyOneOf' | ||
563 | - $ref: '#/components/parameters/hasHLSFiles' | ||
564 | - $ref: '#/components/parameters/hasWebtorrentFiles' | ||
565 | responses: | ||
566 | '204': | ||
567 | description: successful operation | ||
568 | headers: | ||
569 | Cache-Control: | ||
570 | schema: | ||
571 | type: string | ||
572 | default: 'max-age=900' # 15 min cache | ||
573 | content: | ||
574 | application/xml: | ||
575 | schema: | ||
576 | $ref: '#/components/schemas/VideosForXML' | ||
577 | examples: | ||
578 | nightly: | ||
579 | externalValue: https://peertube2.cpy.re/feeds/videos.xml?filter=local | ||
580 | application/rss+xml: | ||
581 | schema: | ||
582 | $ref: '#/components/schemas/VideosForXML' | ||
583 | examples: | ||
584 | nightly: | ||
585 | externalValue: https://peertube2.cpy.re/feeds/videos.rss?filter=local | ||
586 | text/xml: | ||
587 | schema: | ||
588 | $ref: '#/components/schemas/VideosForXML' | ||
589 | examples: | ||
590 | nightly: | ||
591 | externalValue: https://peertube2.cpy.re/feeds/videos.xml?filter=local | ||
592 | application/atom+xml: | ||
593 | schema: | ||
594 | $ref: '#/components/schemas/VideosForXML' | ||
595 | examples: | ||
596 | nightly: | ||
597 | externalValue: https://peertube2.cpy.re/feeds/videos.atom?filter=local | ||
598 | application/json: | ||
599 | schema: | ||
600 | type: object | ||
601 | examples: | ||
602 | nightly: | ||
603 | externalValue: https://peertube2.cpy.re/feeds/videos.json?filter=local | ||
604 | '404': | ||
605 | description: video channel or account not found | ||
606 | '406': | ||
607 | description: accept header unsupported | ||
608 | |||
609 | '/feeds/subscriptions.{format}': | ||
610 | get: | ||
611 | tags: | ||
612 | - Video Feeds | ||
613 | summary: List videos of subscriptions tied to a token | ||
614 | operationId: getSyndicatedSubscriptionVideos | ||
615 | parameters: | ||
616 | - name: format | ||
617 | in: path | ||
618 | required: true | ||
619 | description: 'format expected (we focus on making `rss` the most featureful ; it serves [Media RSS](https://www.rssboard.org/media-rss))' | ||
620 | schema: | ||
621 | type: string | ||
622 | enum: | ||
623 | - xml | ||
624 | - rss | ||
625 | - rss2 | ||
626 | - atom | ||
627 | - atom1 | ||
628 | - json | ||
629 | - json1 | ||
630 | - name: accountId | ||
631 | in: query | ||
632 | description: limit listing to a specific account | ||
633 | schema: | ||
634 | type: string | ||
635 | required: true | ||
636 | - name: token | ||
637 | in: query | ||
638 | description: private token allowing access | ||
639 | schema: | ||
640 | type: string | ||
641 | required: true | ||
642 | - $ref: '#/components/parameters/sort' | ||
643 | - $ref: '#/components/parameters/nsfw' | ||
644 | - $ref: '#/components/parameters/isLocal' | ||
645 | - $ref: '#/components/parameters/include' | ||
646 | - $ref: '#/components/parameters/privacyOneOf' | ||
647 | - $ref: '#/components/parameters/hasHLSFiles' | ||
648 | - $ref: '#/components/parameters/hasWebtorrentFiles' | ||
649 | responses: | ||
650 | '204': | ||
651 | description: successful operation | ||
652 | headers: | ||
653 | Cache-Control: | ||
654 | schema: | ||
655 | type: string | ||
656 | default: 'max-age=900' # 15 min cache | ||
657 | content: | ||
658 | application/xml: | ||
659 | schema: | ||
660 | $ref: '#/components/schemas/VideosForXML' | ||
661 | application/rss+xml: | ||
662 | schema: | ||
663 | $ref: '#/components/schemas/VideosForXML' | ||
664 | text/xml: | ||
665 | schema: | ||
666 | $ref: '#/components/schemas/VideosForXML' | ||
667 | application/atom+xml: | ||
668 | schema: | ||
669 | $ref: '#/components/schemas/VideosForXML' | ||
670 | application/json: | ||
671 | schema: | ||
672 | type: object | ||
673 | '406': | ||
674 | description: accept header unsupported | ||
675 | |||
676 | '/api/v1/accounts/{name}': | ||
353 | get: | 677 | get: |
354 | tags: | 678 | tags: |
355 | - Accounts | 679 | - Accounts |
@@ -367,7 +691,7 @@ paths: | |||
367 | '404': | 691 | '404': |
368 | description: account not found | 692 | description: account not found |
369 | 693 | ||
370 | '/accounts/{name}/videos': | 694 | '/api/v1/accounts/{name}/videos': |
371 | get: | 695 | get: |
372 | tags: | 696 | tags: |
373 | - Accounts | 697 | - Accounts |
@@ -434,7 +758,7 @@ paths: | |||
434 | 758 | ||
435 | print(json) | 759 | print(json) |
436 | 760 | ||
437 | '/accounts/{name}/followers': | 761 | '/api/v1/accounts/{name}/followers': |
438 | get: | 762 | get: |
439 | tags: | 763 | tags: |
440 | - Accounts | 764 | - Accounts |
@@ -464,7 +788,7 @@ paths: | |||
464 | items: | 788 | items: |
465 | $ref: '#/components/schemas/Follow' | 789 | $ref: '#/components/schemas/Follow' |
466 | 790 | ||
467 | /accounts: | 791 | /api/v1/accounts: |
468 | get: | 792 | get: |
469 | tags: | 793 | tags: |
470 | - Accounts | 794 | - Accounts |
@@ -484,7 +808,7 @@ paths: | |||
484 | items: | 808 | items: |
485 | $ref: '#/components/schemas/Account' | 809 | $ref: '#/components/schemas/Account' |
486 | 810 | ||
487 | /config: | 811 | /api/v1/config: |
488 | get: | 812 | get: |
489 | tags: | 813 | tags: |
490 | - Config | 814 | - Config |
@@ -501,7 +825,7 @@ paths: | |||
501 | nightly: | 825 | nightly: |
502 | externalValue: https://peertube2.cpy.re/api/v1/config | 826 | externalValue: https://peertube2.cpy.re/api/v1/config |
503 | 827 | ||
504 | /config/about: | 828 | /api/v1/config/about: |
505 | get: | 829 | get: |
506 | summary: Get instance "About" information | 830 | summary: Get instance "About" information |
507 | operationId: getAbout | 831 | operationId: getAbout |
@@ -518,7 +842,7 @@ paths: | |||
518 | nightly: | 842 | nightly: |
519 | externalValue: https://peertube2.cpy.re/api/v1/config/about | 843 | externalValue: https://peertube2.cpy.re/api/v1/config/about |
520 | 844 | ||
521 | /config/custom: | 845 | /api/v1/config/custom: |
522 | get: | 846 | get: |
523 | summary: Get instance runtime configuration | 847 | summary: Get instance runtime configuration |
524 | operationId: getCustomConfig | 848 | operationId: getCustomConfig |
@@ -563,7 +887,7 @@ paths: | |||
563 | '200': | 887 | '200': |
564 | description: successful operation | 888 | description: successful operation |
565 | 889 | ||
566 | /custom-pages/homepage/instance: | 890 | /api/v1/custom-pages/homepage/instance: |
567 | get: | 891 | get: |
568 | summary: Get instance custom homepage | 892 | summary: Get instance custom homepage |
569 | tags: | 893 | tags: |
@@ -597,7 +921,7 @@ paths: | |||
597 | '204': | 921 | '204': |
598 | description: successful operation | 922 | description: successful operation |
599 | 923 | ||
600 | /jobs/pause: | 924 | /api/v1/jobs/pause: |
601 | post: | 925 | post: |
602 | summary: Pause job queue | 926 | summary: Pause job queue |
603 | security: | 927 | security: |
@@ -609,7 +933,7 @@ paths: | |||
609 | '204': | 933 | '204': |
610 | description: successful operation | 934 | description: successful operation |
611 | 935 | ||
612 | /jobs/resume: | 936 | /api/v1/jobs/resume: |
613 | post: | 937 | post: |
614 | summary: Resume job queue | 938 | summary: Resume job queue |
615 | security: | 939 | security: |
@@ -621,7 +945,7 @@ paths: | |||
621 | '204': | 945 | '204': |
622 | description: successful operation | 946 | description: successful operation |
623 | 947 | ||
624 | /jobs/{state}: | 948 | /api/v1/jobs/{state}: |
625 | get: | 949 | get: |
626 | summary: List instance jobs | 950 | summary: List instance jobs |
627 | operationId: getJobs | 951 | operationId: getJobs |
@@ -665,7 +989,7 @@ paths: | |||
665 | items: | 989 | items: |
666 | $ref: '#/components/schemas/Job' | 990 | $ref: '#/components/schemas/Job' |
667 | 991 | ||
668 | /server/followers: | 992 | /api/v1/server/followers: |
669 | get: | 993 | get: |
670 | tags: | 994 | tags: |
671 | - Instance Follows | 995 | - Instance Follows |
@@ -692,7 +1016,7 @@ paths: | |||
692 | items: | 1016 | items: |
693 | $ref: '#/components/schemas/Follow' | 1017 | $ref: '#/components/schemas/Follow' |
694 | 1018 | ||
695 | '/server/followers/{nameWithHost}': | 1019 | '/api/v1/server/followers/{nameWithHost}': |
696 | delete: | 1020 | delete: |
697 | summary: Remove or reject a follower to your server | 1021 | summary: Remove or reject a follower to your server |
698 | security: | 1022 | security: |
@@ -714,7 +1038,7 @@ paths: | |||
714 | '404': | 1038 | '404': |
715 | description: follower not found | 1039 | description: follower not found |
716 | 1040 | ||
717 | '/server/followers/{nameWithHost}/reject': | 1041 | '/api/v1/server/followers/{nameWithHost}/reject': |
718 | post: | 1042 | post: |
719 | summary: Reject a pending follower to your server | 1043 | summary: Reject a pending follower to your server |
720 | security: | 1044 | security: |
@@ -736,7 +1060,7 @@ paths: | |||
736 | '404': | 1060 | '404': |
737 | description: follower not found | 1061 | description: follower not found |
738 | 1062 | ||
739 | '/server/followers/{nameWithHost}/accept': | 1063 | '/api/v1/server/followers/{nameWithHost}/accept': |
740 | post: | 1064 | post: |
741 | summary: Accept a pending follower to your server | 1065 | summary: Accept a pending follower to your server |
742 | security: | 1066 | security: |
@@ -758,7 +1082,7 @@ paths: | |||
758 | '404': | 1082 | '404': |
759 | description: follower not found | 1083 | description: follower not found |
760 | 1084 | ||
761 | /server/following: | 1085 | /api/v1/server/following: |
762 | get: | 1086 | get: |
763 | tags: | 1087 | tags: |
764 | - Instance Follows | 1088 | - Instance Follows |
@@ -814,7 +1138,7 @@ paths: | |||
814 | type: string | 1138 | type: string |
815 | uniqueItems: true | 1139 | uniqueItems: true |
816 | 1140 | ||
817 | '/server/following/{hostOrHandle}': | 1141 | '/api/v1/server/following/{hostOrHandle}': |
818 | delete: | 1142 | delete: |
819 | summary: Unfollow an actor (PeerTube instance, channel or account) | 1143 | summary: Unfollow an actor (PeerTube instance, channel or account) |
820 | security: | 1144 | security: |
@@ -835,7 +1159,7 @@ paths: | |||
835 | '404': | 1159 | '404': |
836 | description: host or handle not found | 1160 | description: host or handle not found |
837 | 1161 | ||
838 | /users: | 1162 | /api/v1/users: |
839 | post: | 1163 | post: |
840 | summary: Create a user | 1164 | summary: Create a user |
841 | operationId: addUser | 1165 | operationId: addUser |
@@ -902,7 +1226,7 @@ paths: | |||
902 | items: | 1226 | items: |
903 | $ref: '#/components/schemas/User' | 1227 | $ref: '#/components/schemas/User' |
904 | 1228 | ||
905 | '/users/{id}': | 1229 | '/api/v1/users/{id}': |
906 | parameters: | 1230 | parameters: |
907 | - $ref: '#/components/parameters/id' | 1231 | - $ref: '#/components/parameters/id' |
908 | delete: | 1232 | delete: |
@@ -958,7 +1282,7 @@ paths: | |||
958 | $ref: '#/components/schemas/UpdateUser' | 1282 | $ref: '#/components/schemas/UpdateUser' |
959 | required: true | 1283 | required: true |
960 | 1284 | ||
961 | /oauth-clients/local: | 1285 | /api/v1/oauth-clients/local: |
962 | get: | 1286 | get: |
963 | summary: Login prerequisite | 1287 | summary: Login prerequisite |
964 | description: You need to retrieve a client id and secret before [logging in](#operation/getOAuthToken). | 1288 | description: You need to retrieve a client id and secret before [logging in](#operation/getOAuthToken). |
@@ -986,7 +1310,7 @@ paths: | |||
986 | ## AUTH | 1310 | ## AUTH |
987 | curl -s "$API/oauth-clients/local" | 1311 | curl -s "$API/oauth-clients/local" |
988 | 1312 | ||
989 | /users/token: | 1313 | /api/v1/users/token: |
990 | post: | 1314 | post: |
991 | summary: Login | 1315 | summary: Login |
992 | operationId: getOAuthToken | 1316 | operationId: getOAuthToken |
@@ -1063,7 +1387,7 @@ paths: | |||
1063 | --data password="$PASSWORD" \ | 1387 | --data password="$PASSWORD" \ |
1064 | | jq -r ".access_token" | 1388 | | jq -r ".access_token" |
1065 | 1389 | ||
1066 | /users/revoke-token: | 1390 | /api/v1/users/revoke-token: |
1067 | post: | 1391 | post: |
1068 | summary: Logout | 1392 | summary: Logout |
1069 | description: Revokes your access token and its associated refresh token, destroying your current session. | 1393 | description: Revokes your access token and its associated refresh token, destroying your current session. |
@@ -1076,7 +1400,7 @@ paths: | |||
1076 | '200': | 1400 | '200': |
1077 | description: successful operation | 1401 | description: successful operation |
1078 | 1402 | ||
1079 | /users/register: | 1403 | /api/v1/users/register: |
1080 | post: | 1404 | post: |
1081 | summary: Register a user | 1405 | summary: Register a user |
1082 | operationId: registerUser | 1406 | operationId: registerUser |
@@ -1093,7 +1417,7 @@ paths: | |||
1093 | $ref: '#/components/schemas/RegisterUser' | 1417 | $ref: '#/components/schemas/RegisterUser' |
1094 | required: true | 1418 | required: true |
1095 | 1419 | ||
1096 | /users/{id}/verify-email: | 1420 | /api/v1/users/{id}/verify-email: |
1097 | post: | 1421 | post: |
1098 | summary: Verify a user | 1422 | summary: Verify a user |
1099 | operationId: verifyUser | 1423 | operationId: verifyUser |
@@ -1126,7 +1450,7 @@ paths: | |||
1126 | '404': | 1450 | '404': |
1127 | description: user not found | 1451 | description: user not found |
1128 | 1452 | ||
1129 | /users/{id}/two-factor/request: | 1453 | /api/v1/users/{id}/two-factor/request: |
1130 | post: | 1454 | post: |
1131 | summary: Request two factor auth | 1455 | summary: Request two factor auth |
1132 | operationId: requestTwoFactor | 1456 | operationId: requestTwoFactor |
@@ -1158,7 +1482,7 @@ paths: | |||
1158 | '404': | 1482 | '404': |
1159 | description: user not found | 1483 | description: user not found |
1160 | 1484 | ||
1161 | /users/{id}/two-factor/confirm-request: | 1485 | /api/v1/users/{id}/two-factor/confirm-request: |
1162 | post: | 1486 | post: |
1163 | summary: Confirm two factor auth | 1487 | summary: Confirm two factor auth |
1164 | operationId: confirmTwoFactorRequest | 1488 | operationId: confirmTwoFactorRequest |
@@ -1190,7 +1514,7 @@ paths: | |||
1190 | '404': | 1514 | '404': |
1191 | description: user not found | 1515 | description: user not found |
1192 | 1516 | ||
1193 | /users/{id}/two-factor/disable: | 1517 | /api/v1/users/{id}/two-factor/disable: |
1194 | post: | 1518 | post: |
1195 | summary: Disable two factor auth | 1519 | summary: Disable two factor auth |
1196 | operationId: disableTwoFactor | 1520 | operationId: disableTwoFactor |
@@ -1217,7 +1541,7 @@ paths: | |||
1217 | description: user not found | 1541 | description: user not found |
1218 | 1542 | ||
1219 | 1543 | ||
1220 | /users/ask-send-verify-email: | 1544 | /api/v1/users/ask-send-verify-email: |
1221 | post: | 1545 | post: |
1222 | summary: Resend user verification link | 1546 | summary: Resend user verification link |
1223 | operationId: resendEmailToVerifyUser | 1547 | operationId: resendEmailToVerifyUser |
@@ -1228,7 +1552,7 @@ paths: | |||
1228 | '204': | 1552 | '204': |
1229 | description: successful operation | 1553 | description: successful operation |
1230 | 1554 | ||
1231 | /users/me: | 1555 | /api/v1/users/me: |
1232 | get: | 1556 | get: |
1233 | summary: Get my user information | 1557 | summary: Get my user information |
1234 | operationId: getUserInfo | 1558 | operationId: getUserInfo |
@@ -1264,7 +1588,7 @@ paths: | |||
1264 | $ref: '#/components/schemas/UpdateMe' | 1588 | $ref: '#/components/schemas/UpdateMe' |
1265 | required: true | 1589 | required: true |
1266 | 1590 | ||
1267 | /users/me/videos/imports: | 1591 | /api/v1/users/me/videos/imports: |
1268 | get: | 1592 | get: |
1269 | summary: Get video imports of my user | 1593 | summary: Get video imports of my user |
1270 | security: | 1594 | security: |
@@ -1306,7 +1630,7 @@ paths: | |||
1306 | schema: | 1630 | schema: |
1307 | $ref: '#/components/schemas/VideoImportsList' | 1631 | $ref: '#/components/schemas/VideoImportsList' |
1308 | 1632 | ||
1309 | /users/me/video-quota-used: | 1633 | /api/v1/users/me/video-quota-used: |
1310 | get: | 1634 | get: |
1311 | summary: Get my user used quota | 1635 | summary: Get my user used quota |
1312 | security: | 1636 | security: |
@@ -1331,7 +1655,7 @@ paths: | |||
1331 | description: The user video quota used today in bytes | 1655 | description: The user video quota used today in bytes |
1332 | example: 1681014151 | 1656 | example: 1681014151 |
1333 | 1657 | ||
1334 | '/users/me/videos/{videoId}/rating': | 1658 | '/api/v1/users/me/videos/{videoId}/rating': |
1335 | get: | 1659 | get: |
1336 | summary: Get rate of my user for a video | 1660 | summary: Get rate of my user for a video |
1337 | security: | 1661 | security: |
@@ -1354,7 +1678,7 @@ paths: | |||
1354 | schema: | 1678 | schema: |
1355 | $ref: '#/components/schemas/GetMeVideoRating' | 1679 | $ref: '#/components/schemas/GetMeVideoRating' |
1356 | 1680 | ||
1357 | /users/me/videos: | 1681 | /api/v1/users/me/videos: |
1358 | get: | 1682 | get: |
1359 | summary: Get videos of my user | 1683 | summary: Get videos of my user |
1360 | security: | 1684 | security: |
@@ -1375,7 +1699,7 @@ paths: | |||
1375 | schema: | 1699 | schema: |
1376 | $ref: '#/components/schemas/VideoListResponse' | 1700 | $ref: '#/components/schemas/VideoListResponse' |
1377 | 1701 | ||
1378 | /users/me/subscriptions: | 1702 | /api/v1/users/me/subscriptions: |
1379 | get: | 1703 | get: |
1380 | summary: Get my user subscriptions | 1704 | summary: Get my user subscriptions |
1381 | security: | 1705 | security: |
@@ -1421,7 +1745,7 @@ paths: | |||
1421 | '200': | 1745 | '200': |
1422 | description: successful operation | 1746 | description: successful operation |
1423 | 1747 | ||
1424 | /users/me/subscriptions/exist: | 1748 | /api/v1/users/me/subscriptions/exist: |
1425 | get: | 1749 | get: |
1426 | summary: Get if subscriptions exist for my user | 1750 | summary: Get if subscriptions exist for my user |
1427 | security: | 1751 | security: |
@@ -1439,7 +1763,7 @@ paths: | |||
1439 | schema: | 1763 | schema: |
1440 | type: object | 1764 | type: object |
1441 | 1765 | ||
1442 | /users/me/subscriptions/videos: | 1766 | /api/v1/users/me/subscriptions/videos: |
1443 | get: | 1767 | get: |
1444 | summary: List videos of subscriptions of my user | 1768 | summary: List videos of subscriptions of my user |
1445 | security: | 1769 | security: |
@@ -1473,7 +1797,7 @@ paths: | |||
1473 | schema: | 1797 | schema: |
1474 | $ref: '#/components/schemas/VideoListResponse' | 1798 | $ref: '#/components/schemas/VideoListResponse' |
1475 | 1799 | ||
1476 | '/users/me/subscriptions/{subscriptionHandle}': | 1800 | '/api/v1/users/me/subscriptions/{subscriptionHandle}': |
1477 | get: | 1801 | get: |
1478 | summary: Get subscription of my user | 1802 | summary: Get subscription of my user |
1479 | security: | 1803 | security: |
@@ -1503,7 +1827,7 @@ paths: | |||
1503 | '200': | 1827 | '200': |
1504 | description: successful operation | 1828 | description: successful operation |
1505 | 1829 | ||
1506 | /users/me/notifications: | 1830 | /api/v1/users/me/notifications: |
1507 | get: | 1831 | get: |
1508 | summary: List my notifications | 1832 | summary: List my notifications |
1509 | security: | 1833 | security: |
@@ -1527,7 +1851,7 @@ paths: | |||
1527 | schema: | 1851 | schema: |
1528 | $ref: '#/components/schemas/NotificationListResponse' | 1852 | $ref: '#/components/schemas/NotificationListResponse' |
1529 | 1853 | ||
1530 | /users/me/notifications/read: | 1854 | /api/v1/users/me/notifications/read: |
1531 | post: | 1855 | post: |
1532 | summary: Mark notifications as read by their id | 1856 | summary: Mark notifications as read by their id |
1533 | security: | 1857 | security: |
@@ -1551,7 +1875,7 @@ paths: | |||
1551 | '204': | 1875 | '204': |
1552 | description: successful operation | 1876 | description: successful operation |
1553 | 1877 | ||
1554 | /users/me/notifications/read-all: | 1878 | /api/v1/users/me/notifications/read-all: |
1555 | post: | 1879 | post: |
1556 | summary: Mark all my notification as read | 1880 | summary: Mark all my notification as read |
1557 | security: | 1881 | security: |
@@ -1562,7 +1886,7 @@ paths: | |||
1562 | '204': | 1886 | '204': |
1563 | description: successful operation | 1887 | description: successful operation |
1564 | 1888 | ||
1565 | /users/me/notification-settings: | 1889 | /api/v1/users/me/notification-settings: |
1566 | put: | 1890 | put: |
1567 | summary: Update my notification settings | 1891 | summary: Update my notification settings |
1568 | security: | 1892 | security: |
@@ -1603,7 +1927,7 @@ paths: | |||
1603 | '204': | 1927 | '204': |
1604 | description: successful operation | 1928 | description: successful operation |
1605 | 1929 | ||
1606 | /users/me/history/videos: | 1930 | /api/v1/users/me/history/videos: |
1607 | get: | 1931 | get: |
1608 | summary: List watched videos history | 1932 | summary: List watched videos history |
1609 | security: | 1933 | security: |
@@ -1622,7 +1946,7 @@ paths: | |||
1622 | schema: | 1946 | schema: |
1623 | $ref: '#/components/schemas/VideoListResponse' | 1947 | $ref: '#/components/schemas/VideoListResponse' |
1624 | 1948 | ||
1625 | /users/me/history/videos/{videoId}: | 1949 | /api/v1/users/me/history/videos/{videoId}: |
1626 | delete: | 1950 | delete: |
1627 | summary: Delete history element | 1951 | summary: Delete history element |
1628 | security: | 1952 | security: |
@@ -1639,7 +1963,7 @@ paths: | |||
1639 | '204': | 1963 | '204': |
1640 | description: successful operation | 1964 | description: successful operation |
1641 | 1965 | ||
1642 | /users/me/history/videos/remove: | 1966 | /api/v1/users/me/history/videos/remove: |
1643 | post: | 1967 | post: |
1644 | summary: Clear video history | 1968 | summary: Clear video history |
1645 | security: | 1969 | security: |
@@ -1660,7 +1984,7 @@ paths: | |||
1660 | '204': | 1984 | '204': |
1661 | description: successful operation | 1985 | description: successful operation |
1662 | 1986 | ||
1663 | /users/me/avatar/pick: | 1987 | /api/v1/users/me/avatar/pick: |
1664 | post: | 1988 | post: |
1665 | summary: Update my user avatar | 1989 | summary: Update my user avatar |
1666 | security: | 1990 | security: |
@@ -1701,7 +2025,7 @@ paths: | |||
1701 | avatarfile: | 2025 | avatarfile: |
1702 | contentType: image/png, image/jpeg | 2026 | contentType: image/png, image/jpeg |
1703 | 2027 | ||
1704 | /users/me/avatar: | 2028 | /api/v1/users/me/avatar: |
1705 | delete: | 2029 | delete: |
1706 | summary: Delete my avatar | 2030 | summary: Delete my avatar |
1707 | security: | 2031 | security: |
@@ -1712,7 +2036,7 @@ paths: | |||
1712 | '204': | 2036 | '204': |
1713 | description: successful operation | 2037 | description: successful operation |
1714 | 2038 | ||
1715 | /videos/ownership: | 2039 | /api/v1/videos/ownership: |
1716 | get: | 2040 | get: |
1717 | summary: List video ownership changes | 2041 | summary: List video ownership changes |
1718 | tags: | 2042 | tags: |
@@ -1723,7 +2047,7 @@ paths: | |||
1723 | '200': | 2047 | '200': |
1724 | description: successful operation | 2048 | description: successful operation |
1725 | 2049 | ||
1726 | '/videos/ownership/{id}/accept': | 2050 | '/api/v1/videos/ownership/{id}/accept': |
1727 | post: | 2051 | post: |
1728 | summary: Accept ownership change request | 2052 | summary: Accept ownership change request |
1729 | tags: | 2053 | tags: |
@@ -1740,7 +2064,7 @@ paths: | |||
1740 | '404': | 2064 | '404': |
1741 | description: video ownership change not found | 2065 | description: video ownership change not found |
1742 | 2066 | ||
1743 | '/videos/ownership/{id}/refuse': | 2067 | '/api/v1/videos/ownership/{id}/refuse': |
1744 | post: | 2068 | post: |
1745 | summary: Refuse ownership change request | 2069 | summary: Refuse ownership change request |
1746 | tags: | 2070 | tags: |
@@ -1757,7 +2081,7 @@ paths: | |||
1757 | '404': | 2081 | '404': |
1758 | description: video ownership change not found | 2082 | description: video ownership change not found |
1759 | 2083 | ||
1760 | '/videos/{id}/give-ownership': | 2084 | '/api/v1/videos/{id}/give-ownership': |
1761 | post: | 2085 | post: |
1762 | summary: Request ownership change | 2086 | summary: Request ownership change |
1763 | tags: | 2087 | tags: |
@@ -1785,7 +2109,30 @@ paths: | |||
1785 | '404': | 2109 | '404': |
1786 | description: video not found | 2110 | description: video not found |
1787 | 2111 | ||
1788 | /videos/{id}/studio/edit: | 2112 | '/api/v1/videos/{id}/token': |
2113 | post: | ||
2114 | summary: Request video token | ||
2115 | operationId: requestVideoToken | ||
2116 | description: Request special tokens that expire quickly to use them in some context (like accessing private static files) | ||
2117 | tags: | ||
2118 | - Video | ||
2119 | security: | ||
2120 | - OAuth2: [] | ||
2121 | parameters: | ||
2122 | - $ref: '#/components/parameters/idOrUUID' | ||
2123 | responses: | ||
2124 | '200': | ||
2125 | description: successful operation | ||
2126 | content: | ||
2127 | application/json: | ||
2128 | schema: | ||
2129 | $ref: '#/components/schemas/VideoTokenResponse' | ||
2130 | '400': | ||
2131 | description: incorrect parameters | ||
2132 | '404': | ||
2133 | description: video not found | ||
2134 | |||
2135 | /api/v1/videos/{id}/studio/edit: | ||
1789 | post: | 2136 | post: |
1790 | summary: Create a studio task | 2137 | summary: Create a studio task |
1791 | tags: | 2138 | tags: |
@@ -1810,7 +2157,7 @@ paths: | |||
1810 | '404': | 2157 | '404': |
1811 | description: video not found | 2158 | description: video not found |
1812 | 2159 | ||
1813 | /videos: | 2160 | /api/v1/videos: |
1814 | get: | 2161 | get: |
1815 | summary: List videos | 2162 | summary: List videos |
1816 | operationId: getVideos | 2163 | operationId: getVideos |
@@ -1841,7 +2188,7 @@ paths: | |||
1841 | schema: | 2188 | schema: |
1842 | $ref: '#/components/schemas/VideoListResponse' | 2189 | $ref: '#/components/schemas/VideoListResponse' |
1843 | 2190 | ||
1844 | /videos/categories: | 2191 | /api/v1/videos/categories: |
1845 | get: | 2192 | get: |
1846 | summary: List available video categories | 2193 | summary: List available video categories |
1847 | operationId: getCategories | 2194 | operationId: getCategories |
@@ -1860,7 +2207,7 @@ paths: | |||
1860 | nightly: | 2207 | nightly: |
1861 | externalValue: https://peertube2.cpy.re/api/v1/videos/categories | 2208 | externalValue: https://peertube2.cpy.re/api/v1/videos/categories |
1862 | 2209 | ||
1863 | /videos/licences: | 2210 | /api/v1/videos/licences: |
1864 | get: | 2211 | get: |
1865 | summary: List available video licences | 2212 | summary: List available video licences |
1866 | operationId: getLicences | 2213 | operationId: getLicences |
@@ -1879,7 +2226,7 @@ paths: | |||
1879 | nightly: | 2226 | nightly: |
1880 | externalValue: https://peertube2.cpy.re/api/v1/videos/licences | 2227 | externalValue: https://peertube2.cpy.re/api/v1/videos/licences |
1881 | 2228 | ||
1882 | /videos/languages: | 2229 | /api/v1/videos/languages: |
1883 | get: | 2230 | get: |
1884 | summary: List available video languages | 2231 | summary: List available video languages |
1885 | operationId: getLanguages | 2232 | operationId: getLanguages |
@@ -1898,7 +2245,7 @@ paths: | |||
1898 | nightly: | 2245 | nightly: |
1899 | externalValue: https://peertube2.cpy.re/api/v1/videos/languages | 2246 | externalValue: https://peertube2.cpy.re/api/v1/videos/languages |
1900 | 2247 | ||
1901 | /videos/privacies: | 2248 | /api/v1/videos/privacies: |
1902 | get: | 2249 | get: |
1903 | summary: List available video privacy policies | 2250 | summary: List available video privacy policies |
1904 | operationId: getPrivacyPolicies | 2251 | operationId: getPrivacyPolicies |
@@ -1917,7 +2264,7 @@ paths: | |||
1917 | nightly: | 2264 | nightly: |
1918 | externalValue: https://peertube2.cpy.re/api/v1/videos/privacies | 2265 | externalValue: https://peertube2.cpy.re/api/v1/videos/privacies |
1919 | 2266 | ||
1920 | '/videos/{id}': | 2267 | '/api/v1/videos/{id}': |
1921 | put: | 2268 | put: |
1922 | summary: Update a video | 2269 | summary: Update a video |
1923 | operationId: putVideo | 2270 | operationId: putVideo |
@@ -2023,7 +2370,7 @@ paths: | |||
2023 | '204': | 2370 | '204': |
2024 | description: successful operation | 2371 | description: successful operation |
2025 | 2372 | ||
2026 | '/videos/{id}/description': | 2373 | '/api/v1/videos/{id}/description': |
2027 | get: | 2374 | get: |
2028 | summary: Get complete video description | 2375 | summary: Get complete video description |
2029 | operationId: getVideoDesc | 2376 | operationId: getVideoDesc |
@@ -2044,7 +2391,7 @@ paths: | |||
2044 | example: | | 2391 | example: | |
2045 | **[Want to help to translate this video?](https://weblate.framasoft.org/projects/what-is-peertube-video/)**\r\n\r\n**Take back the control of your videos! [#JoinPeertube](https://joinpeertube.org)** | 2392 | **[Want to help to translate this video?](https://weblate.framasoft.org/projects/what-is-peertube-video/)**\r\n\r\n**Take back the control of your videos! [#JoinPeertube](https://joinpeertube.org)** |
2046 | 2393 | ||
2047 | '/videos/{id}/source': | 2394 | '/api/v1/videos/{id}/source': |
2048 | post: | 2395 | post: |
2049 | summary: Get video source file metadata | 2396 | summary: Get video source file metadata |
2050 | operationId: getVideoSource | 2397 | operationId: getVideoSource |
@@ -2060,7 +2407,7 @@ paths: | |||
2060 | schema: | 2407 | schema: |
2061 | $ref: '#/components/schemas/VideoSource' | 2408 | $ref: '#/components/schemas/VideoSource' |
2062 | 2409 | ||
2063 | '/videos/{id}/views': | 2410 | '/api/v1/videos/{id}/views': |
2064 | post: | 2411 | post: |
2065 | summary: Notify user is watching a video | 2412 | summary: Notify user is watching a video |
2066 | description: Call this endpoint regularly (every 5-10 seconds for example) to notify the server the user is watching the video. After a while, PeerTube will increase video's viewers counter. If the user is authenticated, PeerTube will also store the current player time. | 2413 | description: Call this endpoint regularly (every 5-10 seconds for example) to notify the server the user is watching the video. After a while, PeerTube will increase video's viewers counter. If the user is authenticated, PeerTube will also store the current player time. |
@@ -2079,7 +2426,7 @@ paths: | |||
2079 | '204': | 2426 | '204': |
2080 | description: successful operation | 2427 | description: successful operation |
2081 | 2428 | ||
2082 | '/videos/{id}/watching': | 2429 | '/api/v1/videos/{id}/watching': |
2083 | put: | 2430 | put: |
2084 | summary: Set watching progress of a video | 2431 | summary: Set watching progress of a video |
2085 | deprecated: true | 2432 | deprecated: true |
@@ -2100,7 +2447,7 @@ paths: | |||
2100 | '204': | 2447 | '204': |
2101 | description: successful operation | 2448 | description: successful operation |
2102 | 2449 | ||
2103 | '/videos/{id}/stats/overall': | 2450 | '/api/v1/videos/{id}/stats/overall': |
2104 | get: | 2451 | get: |
2105 | summary: Get overall stats of a video | 2452 | summary: Get overall stats of a video |
2106 | tags: | 2453 | tags: |
@@ -2129,7 +2476,7 @@ paths: | |||
2129 | schema: | 2476 | schema: |
2130 | $ref: '#/components/schemas/VideoStatsOverall' | 2477 | $ref: '#/components/schemas/VideoStatsOverall' |
2131 | 2478 | ||
2132 | '/videos/{id}/stats/retention': | 2479 | '/api/v1/videos/{id}/stats/retention': |
2133 | get: | 2480 | get: |
2134 | summary: Get retention stats of a video | 2481 | summary: Get retention stats of a video |
2135 | tags: | 2482 | tags: |
@@ -2146,7 +2493,7 @@ paths: | |||
2146 | schema: | 2493 | schema: |
2147 | $ref: '#/components/schemas/VideoStatsRetention' | 2494 | $ref: '#/components/schemas/VideoStatsRetention' |
2148 | 2495 | ||
2149 | '/videos/{id}/stats/timeseries/{metric}': | 2496 | '/api/v1/videos/{id}/stats/timeseries/{metric}': |
2150 | get: | 2497 | get: |
2151 | summary: Get timeserie stats of a video | 2498 | summary: Get timeserie stats of a video |
2152 | tags: | 2499 | tags: |
@@ -2185,7 +2532,7 @@ paths: | |||
2185 | schema: | 2532 | schema: |
2186 | $ref: '#/components/schemas/VideoStatsTimeserie' | 2533 | $ref: '#/components/schemas/VideoStatsTimeserie' |
2187 | 2534 | ||
2188 | /videos/upload: | 2535 | /api/v1/videos/upload: |
2189 | post: | 2536 | post: |
2190 | summary: Upload a video | 2537 | summary: Upload a video |
2191 | description: Uses a single request to upload a video. | 2538 | description: Uses a single request to upload a video. |
@@ -2263,7 +2610,7 @@ paths: | |||
2263 | --form channelId=$CHANNEL_ID \ | 2610 | --form channelId=$CHANNEL_ID \ |
2264 | --form name="$NAME" | 2611 | --form name="$NAME" |
2265 | 2612 | ||
2266 | /videos/upload-resumable: | 2613 | /api/v1/videos/upload-resumable: |
2267 | post: | 2614 | post: |
2268 | summary: Initialize the resumable upload of a video | 2615 | summary: Initialize the resumable upload of a video |
2269 | description: Uses [a resumable protocol](https://github.com/kukhariev/node-uploadx/blob/master/proto.md) to initialize the upload of a video | 2616 | description: Uses [a resumable protocol](https://github.com/kukhariev/node-uploadx/blob/master/proto.md) to initialize the upload of a video |
@@ -2437,7 +2784,7 @@ paths: | |||
2437 | '404': | 2784 | '404': |
2438 | description: upload not found | 2785 | description: upload not found |
2439 | 2786 | ||
2440 | /videos/imports: | 2787 | /api/v1/videos/imports: |
2441 | post: | 2788 | post: |
2442 | summary: Import a video | 2789 | summary: Import a video |
2443 | description: Import a torrent or magnetURI or HTTP resource (if enabled by the instance administrator) | 2790 | description: Import a torrent or magnetURI or HTTP resource (if enabled by the instance administrator) |
@@ -2473,7 +2820,7 @@ paths: | |||
2473 | '409': | 2820 | '409': |
2474 | description: HTTP or Torrent/magnetURI import not enabled | 2821 | description: HTTP or Torrent/magnetURI import not enabled |
2475 | 2822 | ||
2476 | /videos/imports/{id}/cancel: | 2823 | /api/v1/videos/imports/{id}/cancel: |
2477 | post: | 2824 | post: |
2478 | summary: Cancel video import | 2825 | summary: Cancel video import |
2479 | description: Cancel a pending video import | 2826 | description: Cancel a pending video import |
@@ -2487,7 +2834,7 @@ paths: | |||
2487 | '204': | 2834 | '204': |
2488 | description: successful operation | 2835 | description: successful operation |
2489 | 2836 | ||
2490 | /videos/imports/{id}: | 2837 | /api/v1/videos/imports/{id}: |
2491 | delete: | 2838 | delete: |
2492 | summary: Delete video import | 2839 | summary: Delete video import |
2493 | description: Delete ended video import | 2840 | description: Delete ended video import |
@@ -2501,7 +2848,7 @@ paths: | |||
2501 | '204': | 2848 | '204': |
2502 | description: successful operation | 2849 | description: successful operation |
2503 | 2850 | ||
2504 | /videos/live: | 2851 | /api/v1/videos/live: |
2505 | post: | 2852 | post: |
2506 | summary: Create a live | 2853 | summary: Create a live |
2507 | operationId: addLive | 2854 | operationId: addLive |
@@ -2603,7 +2950,7 @@ paths: | |||
2603 | previewfile: | 2950 | previewfile: |
2604 | contentType: image/jpeg | 2951 | contentType: image/jpeg |
2605 | 2952 | ||
2606 | /videos/live/{id}: | 2953 | /api/v1/videos/live/{id}: |
2607 | get: | 2954 | get: |
2608 | summary: Get information about a live | 2955 | summary: Get information about a live |
2609 | operationId: getLiveId | 2956 | operationId: getLiveId |
@@ -2643,7 +2990,7 @@ paths: | |||
2643 | description: bad parameters or trying to update a live that has already started | 2990 | description: bad parameters or trying to update a live that has already started |
2644 | '403': | 2991 | '403': |
2645 | description: trying to save replay of the live but saving replay is not enabled on the instance | 2992 | description: trying to save replay of the live but saving replay is not enabled on the instance |
2646 | /videos/live/{id}/sessions: | 2993 | /api/v1/videos/live/{id}/sessions: |
2647 | get: | 2994 | get: |
2648 | summary: List live sessions | 2995 | summary: List live sessions |
2649 | description: List all sessions created in a particular live | 2996 | description: List all sessions created in a particular live |
@@ -2668,7 +3015,7 @@ paths: | |||
2668 | type: array | 3015 | type: array |
2669 | items: | 3016 | items: |
2670 | $ref: '#/components/schemas/LiveVideoSessionResponse' | 3017 | $ref: '#/components/schemas/LiveVideoSessionResponse' |
2671 | /videos/{id}/live-session: | 3018 | /api/v1/videos/{id}/live-session: |
2672 | get: | 3019 | get: |
2673 | summary: Get live session of a replay | 3020 | summary: Get live session of a replay |
2674 | description: If the video is a replay of a live, you can find the associated live session using this endpoint | 3021 | description: If the video is a replay of a live, you can find the associated live session using this endpoint |
@@ -2686,7 +3033,7 @@ paths: | |||
2686 | schema: | 3033 | schema: |
2687 | $ref: '#/components/schemas/LiveVideoSessionResponse' | 3034 | $ref: '#/components/schemas/LiveVideoSessionResponse' |
2688 | 3035 | ||
2689 | /users/me/abuses: | 3036 | /api/v1/users/me/abuses: |
2690 | get: | 3037 | get: |
2691 | summary: List my abuses | 3038 | summary: List my abuses |
2692 | operationId: getMyAbuses | 3039 | operationId: getMyAbuses |
@@ -2724,7 +3071,7 @@ paths: | |||
2724 | items: | 3071 | items: |
2725 | $ref: '#/components/schemas/Abuse' | 3072 | $ref: '#/components/schemas/Abuse' |
2726 | 3073 | ||
2727 | /abuses: | 3074 | /api/v1/abuses: |
2728 | get: | 3075 | get: |
2729 | summary: List abuses | 3076 | summary: List abuses |
2730 | operationId: getAbuses | 3077 | operationId: getAbuses |
@@ -2877,7 +3224,7 @@ paths: | |||
2877 | '400': | 3224 | '400': |
2878 | description: incorrect request parameters | 3225 | description: incorrect request parameters |
2879 | 3226 | ||
2880 | '/abuses/{abuseId}': | 3227 | '/api/v1/abuses/{abuseId}': |
2881 | put: | 3228 | put: |
2882 | summary: Update an abuse | 3229 | summary: Update an abuse |
2883 | security: | 3230 | security: |
@@ -2922,7 +3269,7 @@ paths: | |||
2922 | '404': | 3269 | '404': |
2923 | description: block not found | 3270 | description: block not found |
2924 | 3271 | ||
2925 | '/abuses/{abuseId}/messages': | 3272 | '/api/v1/abuses/{abuseId}/messages': |
2926 | get: | 3273 | get: |
2927 | summary: List messages of an abuse | 3274 | summary: List messages of an abuse |
2928 | security: | 3275 | security: |
@@ -2974,7 +3321,7 @@ paths: | |||
2974 | '400': | 3321 | '400': |
2975 | description: incorrect request parameters | 3322 | description: incorrect request parameters |
2976 | 3323 | ||
2977 | '/abuses/{abuseId}/messages/{abuseMessageId}': | 3324 | '/api/v1/abuses/{abuseId}/messages/{abuseMessageId}': |
2978 | delete: | 3325 | delete: |
2979 | summary: Delete an abuse message | 3326 | summary: Delete an abuse message |
2980 | security: | 3327 | security: |
@@ -2988,7 +3335,7 @@ paths: | |||
2988 | '204': | 3335 | '204': |
2989 | description: successful operation | 3336 | description: successful operation |
2990 | 3337 | ||
2991 | '/videos/{id}/blacklist': | 3338 | '/api/v1/videos/{id}/blacklist': |
2992 | post: | 3339 | post: |
2993 | summary: Block a video | 3340 | summary: Block a video |
2994 | operationId: addVideoBlock | 3341 | operationId: addVideoBlock |
@@ -3020,7 +3367,7 @@ paths: | |||
3020 | '404': | 3367 | '404': |
3021 | description: block not found | 3368 | description: block not found |
3022 | 3369 | ||
3023 | /videos/blacklist: | 3370 | /api/v1/videos/blacklist: |
3024 | get: | 3371 | get: |
3025 | tags: | 3372 | tags: |
3026 | - Video Blocks | 3373 | - Video Blocks |
@@ -3068,7 +3415,7 @@ paths: | |||
3068 | items: | 3415 | items: |
3069 | $ref: '#/components/schemas/VideoBlacklist' | 3416 | $ref: '#/components/schemas/VideoBlacklist' |
3070 | 3417 | ||
3071 | /videos/{id}/captions: | 3418 | /api/v1/videos/{id}/captions: |
3072 | get: | 3419 | get: |
3073 | summary: List captions of a video | 3420 | summary: List captions of a video |
3074 | operationId: getVideoCaptions | 3421 | operationId: getVideoCaptions |
@@ -3092,7 +3439,7 @@ paths: | |||
3092 | items: | 3439 | items: |
3093 | $ref: '#/components/schemas/VideoCaption' | 3440 | $ref: '#/components/schemas/VideoCaption' |
3094 | 3441 | ||
3095 | /videos/{id}/captions/{captionLanguage}: | 3442 | /api/v1/videos/{id}/captions/{captionLanguage}: |
3096 | put: | 3443 | put: |
3097 | summary: Add or replace a video caption | 3444 | summary: Add or replace a video caption |
3098 | operationId: addVideoCaption | 3445 | operationId: addVideoCaption |
@@ -3139,7 +3486,7 @@ paths: | |||
3139 | '404': | 3486 | '404': |
3140 | description: video or language or caption for that language not found | 3487 | description: video or language or caption for that language not found |
3141 | 3488 | ||
3142 | /video-channels: | 3489 | /api/v1/video-channels: |
3143 | get: | 3490 | get: |
3144 | summary: List video channels | 3491 | summary: List video channels |
3145 | operationId: getVideoChannels | 3492 | operationId: getVideoChannels |
@@ -3182,7 +3529,7 @@ paths: | |||
3182 | schema: | 3529 | schema: |
3183 | $ref: '#/components/schemas/VideoChannelCreate' | 3530 | $ref: '#/components/schemas/VideoChannelCreate' |
3184 | 3531 | ||
3185 | '/video-channels/{channelHandle}': | 3532 | '/api/v1/video-channels/{channelHandle}': |
3186 | get: | 3533 | get: |
3187 | summary: Get a video channel | 3534 | summary: Get a video channel |
3188 | operationId: getVideoChannel | 3535 | operationId: getVideoChannel |
@@ -3227,7 +3574,7 @@ paths: | |||
3227 | '204': | 3574 | '204': |
3228 | description: successful operation | 3575 | description: successful operation |
3229 | 3576 | ||
3230 | '/video-channels/{channelHandle}/videos': | 3577 | '/api/v1/video-channels/{channelHandle}/videos': |
3231 | get: | 3578 | get: |
3232 | summary: List videos of a video channel | 3579 | summary: List videos of a video channel |
3233 | operationId: getVideoChannelVideos | 3580 | operationId: getVideoChannelVideos |
@@ -3260,7 +3607,7 @@ paths: | |||
3260 | schema: | 3607 | schema: |
3261 | $ref: '#/components/schemas/VideoListResponse' | 3608 | $ref: '#/components/schemas/VideoListResponse' |
3262 | 3609 | ||
3263 | '/video-channels/{channelHandle}/followers': | 3610 | '/api/v1/video-channels/{channelHandle}/followers': |
3264 | get: | 3611 | get: |
3265 | tags: | 3612 | tags: |
3266 | - Video Channels | 3613 | - Video Channels |
@@ -3290,7 +3637,7 @@ paths: | |||
3290 | items: | 3637 | items: |
3291 | $ref: '#/components/schemas/Follow' | 3638 | $ref: '#/components/schemas/Follow' |
3292 | 3639 | ||
3293 | '/video-channels/{channelHandle}/avatar/pick': | 3640 | '/api/v1/video-channels/{channelHandle}/avatar/pick': |
3294 | post: | 3641 | post: |
3295 | summary: Update channel avatar | 3642 | summary: Update channel avatar |
3296 | security: | 3643 | security: |
@@ -3333,7 +3680,7 @@ paths: | |||
3333 | avatarfile: | 3680 | avatarfile: |
3334 | contentType: image/png, image/jpeg | 3681 | contentType: image/png, image/jpeg |
3335 | 3682 | ||
3336 | '/video-channels/{channelHandle}/avatar': | 3683 | '/api/v1/video-channels/{channelHandle}/avatar': |
3337 | delete: | 3684 | delete: |
3338 | summary: Delete channel avatar | 3685 | summary: Delete channel avatar |
3339 | security: | 3686 | security: |
@@ -3346,7 +3693,7 @@ paths: | |||
3346 | '204': | 3693 | '204': |
3347 | description: successful operation | 3694 | description: successful operation |
3348 | 3695 | ||
3349 | '/video-channels/{channelHandle}/banner/pick': | 3696 | '/api/v1/video-channels/{channelHandle}/banner/pick': |
3350 | post: | 3697 | post: |
3351 | summary: Update channel banner | 3698 | summary: Update channel banner |
3352 | security: | 3699 | security: |
@@ -3389,7 +3736,7 @@ paths: | |||
3389 | bannerfile: | 3736 | bannerfile: |
3390 | contentType: image/png, image/jpeg | 3737 | contentType: image/png, image/jpeg |
3391 | 3738 | ||
3392 | '/video-channels/{channelHandle}/banner': | 3739 | '/api/v1/video-channels/{channelHandle}/banner': |
3393 | delete: | 3740 | delete: |
3394 | summary: Delete channel banner | 3741 | summary: Delete channel banner |
3395 | security: | 3742 | security: |
@@ -3402,7 +3749,7 @@ paths: | |||
3402 | '204': | 3749 | '204': |
3403 | description: successful operation | 3750 | description: successful operation |
3404 | 3751 | ||
3405 | '/video-channels/{channelHandle}/import-videos': | 3752 | '/api/v1/video-channels/{channelHandle}/import-videos': |
3406 | post: | 3753 | post: |
3407 | summary: Import videos in channel | 3754 | summary: Import videos in channel |
3408 | description: Import a remote channel/playlist videos into a channel | 3755 | description: Import a remote channel/playlist videos into a channel |
@@ -3422,7 +3769,7 @@ paths: | |||
3422 | '204': | 3769 | '204': |
3423 | description: successful operation | 3770 | description: successful operation |
3424 | 3771 | ||
3425 | '/video-channel-syncs': | 3772 | '/api/v1/video-channel-syncs': |
3426 | post: | 3773 | post: |
3427 | summary: Create a synchronization for a video channel | 3774 | summary: Create a synchronization for a video channel |
3428 | operationId: addVideoChannelSync | 3775 | operationId: addVideoChannelSync |
@@ -3446,7 +3793,7 @@ paths: | |||
3446 | videoChannelSync: | 3793 | videoChannelSync: |
3447 | $ref: "#/components/schemas/VideoChannelSync" | 3794 | $ref: "#/components/schemas/VideoChannelSync" |
3448 | 3795 | ||
3449 | '/video-channel-syncs/{channelSyncId}': | 3796 | '/api/v1/video-channel-syncs/{channelSyncId}': |
3450 | delete: | 3797 | delete: |
3451 | summary: Delete a video channel synchronization | 3798 | summary: Delete a video channel synchronization |
3452 | operationId: delVideoChannelSync | 3799 | operationId: delVideoChannelSync |
@@ -3460,7 +3807,7 @@ paths: | |||
3460 | '204': | 3807 | '204': |
3461 | description: successful operation | 3808 | description: successful operation |
3462 | 3809 | ||
3463 | '/video-channel-syncs/{channelSyncId}/sync': | 3810 | '/api/v1/video-channel-syncs/{channelSyncId}/sync': |
3464 | post: | 3811 | post: |
3465 | summary: Triggers the channel synchronization job, fetching all the videos from the remote channel | 3812 | summary: Triggers the channel synchronization job, fetching all the videos from the remote channel |
3466 | operationId: triggerVideoChannelSync | 3813 | operationId: triggerVideoChannelSync |
@@ -3475,7 +3822,7 @@ paths: | |||
3475 | description: successful operation | 3822 | description: successful operation |
3476 | 3823 | ||
3477 | 3824 | ||
3478 | /video-playlists/privacies: | 3825 | /api/v1/video-playlists/privacies: |
3479 | get: | 3826 | get: |
3480 | summary: List available playlist privacy policies | 3827 | summary: List available playlist privacy policies |
3481 | operationId: getPlaylistPrivacyPolicies | 3828 | operationId: getPlaylistPrivacyPolicies |
@@ -3494,7 +3841,7 @@ paths: | |||
3494 | nightly: | 3841 | nightly: |
3495 | externalValue: https://peertube2.cpy.re/api/v1/video-playlists/privacies | 3842 | externalValue: https://peertube2.cpy.re/api/v1/video-playlists/privacies |
3496 | 3843 | ||
3497 | /video-playlists: | 3844 | /api/v1/video-playlists: |
3498 | get: | 3845 | get: |
3499 | summary: List video playlists | 3846 | summary: List video playlists |
3500 | operationId: getPlaylists | 3847 | operationId: getPlaylists |
@@ -3576,7 +3923,7 @@ paths: | |||
3576 | thumbnailfile: | 3923 | thumbnailfile: |
3577 | contentType: image/jpeg | 3924 | contentType: image/jpeg |
3578 | 3925 | ||
3579 | /video-playlists/{playlistId}: | 3926 | /api/v1/video-playlists/{playlistId}: |
3580 | get: | 3927 | get: |
3581 | summary: Get a video playlist | 3928 | summary: Get a video playlist |
3582 | tags: | 3929 | tags: |
@@ -3641,7 +3988,7 @@ paths: | |||
3641 | '204': | 3988 | '204': |
3642 | description: successful operation | 3989 | description: successful operation |
3643 | 3990 | ||
3644 | /video-playlists/{playlistId}/videos: | 3991 | /api/v1/video-playlists/{playlistId}/videos: |
3645 | get: | 3992 | get: |
3646 | summary: 'List videos of a playlist' | 3993 | summary: 'List videos of a playlist' |
3647 | operationId: getVideoPlaylistVideos | 3994 | operationId: getVideoPlaylistVideos |
@@ -3705,7 +4052,7 @@ paths: | |||
3705 | required: | 4052 | required: |
3706 | - videoId | 4053 | - videoId |
3707 | 4054 | ||
3708 | /video-playlists/{playlistId}/videos/reorder: | 4055 | /api/v1/video-playlists/{playlistId}/videos/reorder: |
3709 | post: | 4056 | post: |
3710 | summary: 'Reorder a playlist' | 4057 | summary: 'Reorder a playlist' |
3711 | operationId: reorderVideoPlaylist | 4058 | operationId: reorderVideoPlaylist |
@@ -3740,7 +4087,7 @@ paths: | |||
3740 | - startPosition | 4087 | - startPosition |
3741 | - insertAfterPosition | 4088 | - insertAfterPosition |
3742 | 4089 | ||
3743 | /video-playlists/{playlistId}/videos/{playlistElementId}: | 4090 | /api/v1/video-playlists/{playlistId}/videos/{playlistElementId}: |
3744 | put: | 4091 | put: |
3745 | summary: Update a playlist element | 4092 | summary: Update a playlist element |
3746 | operationId: putVideoPlaylistVideo | 4093 | operationId: putVideoPlaylistVideo |
@@ -3782,7 +4129,7 @@ paths: | |||
3782 | '204': | 4129 | '204': |
3783 | description: successful operation | 4130 | description: successful operation |
3784 | 4131 | ||
3785 | '/users/me/video-playlists/videos-exist': | 4132 | '/api/v1/users/me/video-playlists/videos-exist': |
3786 | get: | 4133 | get: |
3787 | summary: Check video exists in my playlists | 4134 | summary: Check video exists in my playlists |
3788 | security: | 4135 | security: |
@@ -3822,7 +4169,7 @@ paths: | |||
3822 | type: integer | 4169 | type: integer |
3823 | format: seconds | 4170 | format: seconds |
3824 | 4171 | ||
3825 | '/accounts/{name}/video-channels': | 4172 | '/api/v1/accounts/{name}/video-channels': |
3826 | get: | 4173 | get: |
3827 | summary: List video channels of an account | 4174 | summary: List video channels of an account |
3828 | tags: | 4175 | tags: |
@@ -3846,7 +4193,7 @@ paths: | |||
3846 | schema: | 4193 | schema: |
3847 | $ref: '#/components/schemas/VideoChannelList' | 4194 | $ref: '#/components/schemas/VideoChannelList' |
3848 | 4195 | ||
3849 | '/accounts/{name}/video-channel-syncs': | 4196 | '/api/v1/accounts/{name}/video-channel-syncs': |
3850 | get: | 4197 | get: |
3851 | summary: List the synchronizations of video channels of an account | 4198 | summary: List the synchronizations of video channels of an account |
3852 | tags: | 4199 | tags: |
@@ -3866,7 +4213,7 @@ paths: | |||
3866 | schema: | 4213 | schema: |
3867 | $ref: '#/components/schemas/VideoChannelSyncList' | 4214 | $ref: '#/components/schemas/VideoChannelSyncList' |
3868 | 4215 | ||
3869 | '/accounts/{name}/ratings': | 4216 | '/api/v1/accounts/{name}/ratings': |
3870 | get: | 4217 | get: |
3871 | summary: List ratings of an account | 4218 | summary: List ratings of an account |
3872 | security: | 4219 | security: |
@@ -3897,7 +4244,7 @@ paths: | |||
3897 | items: | 4244 | items: |
3898 | $ref: '#/components/schemas/VideoRating' | 4245 | $ref: '#/components/schemas/VideoRating' |
3899 | 4246 | ||
3900 | '/videos/{id}/comment-threads': | 4247 | '/api/v1/videos/{id}/comment-threads': |
3901 | get: | 4248 | get: |
3902 | summary: List threads of a video | 4249 | summary: List threads of a video |
3903 | tags: | 4250 | tags: |
@@ -3945,7 +4292,7 @@ paths: | |||
3945 | required: | 4292 | required: |
3946 | - text | 4293 | - text |
3947 | 4294 | ||
3948 | '/videos/{id}/comment-threads/{threadId}': | 4295 | '/api/v1/videos/{id}/comment-threads/{threadId}': |
3949 | get: | 4296 | get: |
3950 | summary: Get a thread | 4297 | summary: Get a thread |
3951 | tags: | 4298 | tags: |
@@ -3961,7 +4308,7 @@ paths: | |||
3961 | schema: | 4308 | schema: |
3962 | $ref: '#/components/schemas/VideoCommentThreadTree' | 4309 | $ref: '#/components/schemas/VideoCommentThreadTree' |
3963 | 4310 | ||
3964 | '/videos/{id}/comments/{commentId}': | 4311 | '/api/v1/videos/{id}/comments/{commentId}': |
3965 | post: | 4312 | post: |
3966 | summary: Reply to a thread of a video | 4313 | summary: Reply to a thread of a video |
3967 | security: | 4314 | security: |
@@ -4012,7 +4359,7 @@ paths: | |||
4012 | '409': | 4359 | '409': |
4013 | description: comment is already deleted | 4360 | description: comment is already deleted |
4014 | 4361 | ||
4015 | '/videos/{id}/rate': | 4362 | '/api/v1/videos/{id}/rate': |
4016 | put: | 4363 | put: |
4017 | summary: Like/dislike a video | 4364 | summary: Like/dislike a video |
4018 | security: | 4365 | security: |
@@ -4040,7 +4387,7 @@ paths: | |||
4040 | '404': | 4387 | '404': |
4041 | description: video does not exist | 4388 | description: video does not exist |
4042 | 4389 | ||
4043 | '/videos/{id}/hls': | 4390 | '/api/v1/videos/{id}/hls': |
4044 | delete: | 4391 | delete: |
4045 | summary: Delete video HLS files | 4392 | summary: Delete video HLS files |
4046 | security: | 4393 | security: |
@@ -4056,7 +4403,7 @@ paths: | |||
4056 | description: successful operation | 4403 | description: successful operation |
4057 | '404': | 4404 | '404': |
4058 | description: video does not exist | 4405 | description: video does not exist |
4059 | '/videos/{id}/webtorrent': | 4406 | '/api/v1/videos/{id}/webtorrent': |
4060 | delete: | 4407 | delete: |
4061 | summary: Delete video WebTorrent files | 4408 | summary: Delete video WebTorrent files |
4062 | security: | 4409 | security: |
@@ -4073,7 +4420,7 @@ paths: | |||
4073 | '404': | 4420 | '404': |
4074 | description: video does not exist | 4421 | description: video does not exist |
4075 | 4422 | ||
4076 | '/videos/{id}/transcoding': | 4423 | '/api/v1/videos/{id}/transcoding': |
4077 | post: | 4424 | post: |
4078 | summary: Create a transcoding job | 4425 | summary: Create a transcoding job |
4079 | security: | 4426 | security: |
@@ -4103,7 +4450,7 @@ paths: | |||
4103 | '404': | 4450 | '404': |
4104 | description: video does not exist | 4451 | description: video does not exist |
4105 | 4452 | ||
4106 | /search/videos: | 4453 | /api/v1/search/videos: |
4107 | get: | 4454 | get: |
4108 | tags: | 4455 | tags: |
4109 | - Search | 4456 | - Search |
@@ -4184,7 +4531,7 @@ paths: | |||
4184 | '500': | 4531 | '500': |
4185 | description: search index unavailable | 4532 | description: search index unavailable |
4186 | 4533 | ||
4187 | /search/video-channels: | 4534 | /api/v1/search/video-channels: |
4188 | get: | 4535 | get: |
4189 | tags: | 4536 | tags: |
4190 | - Search | 4537 | - Search |
@@ -4217,7 +4564,7 @@ paths: | |||
4217 | '500': | 4564 | '500': |
4218 | description: search index unavailable | 4565 | description: search index unavailable |
4219 | 4566 | ||
4220 | /search/video-playlists: | 4567 | /api/v1/search/video-playlists: |
4221 | get: | 4568 | get: |
4222 | tags: | 4569 | tags: |
4223 | - Search | 4570 | - Search |
@@ -4258,7 +4605,7 @@ paths: | |||
4258 | '500': | 4605 | '500': |
4259 | description: search index unavailable | 4606 | description: search index unavailable |
4260 | 4607 | ||
4261 | /blocklist/status: | 4608 | /api/v1/blocklist/status: |
4262 | get: | 4609 | get: |
4263 | tags: | 4610 | tags: |
4264 | - Account Blocks | 4611 | - Account Blocks |
@@ -4291,7 +4638,7 @@ paths: | |||
4291 | schema: | 4638 | schema: |
4292 | $ref: '#/components/schemas/BlockStatus' | 4639 | $ref: '#/components/schemas/BlockStatus' |
4293 | 4640 | ||
4294 | /server/blocklist/accounts: | 4641 | /api/v1/server/blocklist/accounts: |
4295 | get: | 4642 | get: |
4296 | tags: | 4643 | tags: |
4297 | - Account Blocks | 4644 | - Account Blocks |
@@ -4331,7 +4678,7 @@ paths: | |||
4331 | '409': | 4678 | '409': |
4332 | description: self-blocking forbidden | 4679 | description: self-blocking forbidden |
4333 | 4680 | ||
4334 | '/server/blocklist/accounts/{accountName}': | 4681 | '/api/v1/server/blocklist/accounts/{accountName}': |
4335 | delete: | 4682 | delete: |
4336 | tags: | 4683 | tags: |
4337 | - Account Blocks | 4684 | - Account Blocks |
@@ -4352,7 +4699,7 @@ paths: | |||
4352 | '404': | 4699 | '404': |
4353 | description: account or account block does not exist | 4700 | description: account or account block does not exist |
4354 | 4701 | ||
4355 | /server/blocklist/servers: | 4702 | /api/v1/server/blocklist/servers: |
4356 | get: | 4703 | get: |
4357 | tags: | 4704 | tags: |
4358 | - Server Blocks | 4705 | - Server Blocks |
@@ -4392,7 +4739,7 @@ paths: | |||
4392 | '409': | 4739 | '409': |
4393 | description: self-blocking forbidden | 4740 | description: self-blocking forbidden |
4394 | 4741 | ||
4395 | '/server/blocklist/servers/{host}': | 4742 | '/api/v1/server/blocklist/servers/{host}': |
4396 | delete: | 4743 | delete: |
4397 | tags: | 4744 | tags: |
4398 | - Server Blocks | 4745 | - Server Blocks |
@@ -4414,7 +4761,7 @@ paths: | |||
4414 | '404': | 4761 | '404': |
4415 | description: account block does not exist | 4762 | description: account block does not exist |
4416 | 4763 | ||
4417 | /server/redundancy/{host}: | 4764 | /api/v1/server/redundancy/{host}: |
4418 | put: | 4765 | put: |
4419 | tags: | 4766 | tags: |
4420 | - Instance Redundancy | 4767 | - Instance Redundancy |
@@ -4447,7 +4794,7 @@ paths: | |||
4447 | '404': | 4794 | '404': |
4448 | description: server is not already known | 4795 | description: server is not already known |
4449 | 4796 | ||
4450 | /server/redundancy/videos: | 4797 | /api/v1/server/redundancy/videos: |
4451 | get: | 4798 | get: |
4452 | tags: | 4799 | tags: |
4453 | - Video Mirroring | 4800 | - Video Mirroring |
@@ -4506,7 +4853,7 @@ paths: | |||
4506 | '409': | 4853 | '409': |
4507 | description: video is already mirrored | 4854 | description: video is already mirrored |
4508 | 4855 | ||
4509 | /server/redundancy/videos/{redundancyId}: | 4856 | /api/v1/server/redundancy/videos/{redundancyId}: |
4510 | delete: | 4857 | delete: |
4511 | tags: | 4858 | tags: |
4512 | - Video Mirroring | 4859 | - Video Mirroring |
@@ -4528,7 +4875,7 @@ paths: | |||
4528 | '404': | 4875 | '404': |
4529 | description: video redundancy not found | 4876 | description: video redundancy not found |
4530 | 4877 | ||
4531 | /server/stats: | 4878 | /api/v1/server/stats: |
4532 | get: | 4879 | get: |
4533 | tags: | 4880 | tags: |
4534 | - Stats | 4881 | - Stats |
@@ -4543,7 +4890,7 @@ paths: | |||
4543 | schema: | 4890 | schema: |
4544 | $ref: '#/components/schemas/ServerStats' | 4891 | $ref: '#/components/schemas/ServerStats' |
4545 | 4892 | ||
4546 | /server/logs/client: | 4893 | /api/v1/server/logs/client: |
4547 | post: | 4894 | post: |
4548 | tags: | 4895 | tags: |
4549 | - Logs | 4896 | - Logs |
@@ -4558,7 +4905,7 @@ paths: | |||
4558 | '204': | 4905 | '204': |
4559 | description: successful operation | 4906 | description: successful operation |
4560 | 4907 | ||
4561 | /server/logs: | 4908 | /api/v1/server/logs: |
4562 | get: | 4909 | get: |
4563 | tags: | 4910 | tags: |
4564 | - Logs | 4911 | - Logs |
@@ -4577,7 +4924,7 @@ paths: | |||
4577 | items: | 4924 | items: |
4578 | type: string | 4925 | type: string |
4579 | 4926 | ||
4580 | /server/audit-logs: | 4927 | /api/v1/server/audit-logs: |
4581 | get: | 4928 | get: |
4582 | tags: | 4929 | tags: |
4583 | - Logs | 4930 | - Logs |
@@ -4596,262 +4943,7 @@ paths: | |||
4596 | items: | 4943 | items: |
4597 | type: string | 4944 | type: string |
4598 | 4945 | ||
4599 | '/feeds/video-comments.{format}': | 4946 | /api/v1/plugins: |
4600 | get: | ||
4601 | tags: | ||
4602 | - Feeds | ||
4603 | summary: List comments on videos | ||
4604 | operationId: getSyndicatedComments | ||
4605 | parameters: | ||
4606 | - name: format | ||
4607 | in: path | ||
4608 | required: true | ||
4609 | description: 'format expected (we focus on making `rss` the most featureful ; it serves [Media RSS](https://www.rssboard.org/media-rss))' | ||
4610 | schema: | ||
4611 | type: string | ||
4612 | enum: | ||
4613 | - xml | ||
4614 | - rss | ||
4615 | - rss2 | ||
4616 | - atom | ||
4617 | - atom1 | ||
4618 | - json | ||
4619 | - json1 | ||
4620 | - name: videoId | ||
4621 | in: query | ||
4622 | description: 'limit listing to a specific video' | ||
4623 | schema: | ||
4624 | type: string | ||
4625 | - name: accountId | ||
4626 | in: query | ||
4627 | description: 'limit listing to a specific account' | ||
4628 | schema: | ||
4629 | type: string | ||
4630 | - name: accountName | ||
4631 | in: query | ||
4632 | description: 'limit listing to a specific account' | ||
4633 | schema: | ||
4634 | type: string | ||
4635 | - name: videoChannelId | ||
4636 | in: query | ||
4637 | description: 'limit listing to a specific video channel' | ||
4638 | schema: | ||
4639 | type: string | ||
4640 | - name: videoChannelName | ||
4641 | in: query | ||
4642 | description: 'limit listing to a specific video channel' | ||
4643 | schema: | ||
4644 | type: string | ||
4645 | responses: | ||
4646 | '204': | ||
4647 | description: successful operation | ||
4648 | headers: | ||
4649 | Cache-Control: | ||
4650 | schema: | ||
4651 | type: string | ||
4652 | default: 'max-age=900' # 15 min cache | ||
4653 | content: | ||
4654 | application/xml: | ||
4655 | schema: | ||
4656 | $ref: '#/components/schemas/VideoCommentsForXML' | ||
4657 | examples: | ||
4658 | nightly: | ||
4659 | externalValue: https://peertube2.cpy.re/feeds/video-comments.xml?filter=local | ||
4660 | application/rss+xml: | ||
4661 | schema: | ||
4662 | $ref: '#/components/schemas/VideoCommentsForXML' | ||
4663 | examples: | ||
4664 | nightly: | ||
4665 | externalValue: https://peertube2.cpy.re/feeds/video-comments.rss?filter=local | ||
4666 | text/xml: | ||
4667 | schema: | ||
4668 | $ref: '#/components/schemas/VideoCommentsForXML' | ||
4669 | examples: | ||
4670 | nightly: | ||
4671 | externalValue: https://peertube2.cpy.re/feeds/video-comments.xml?filter=local | ||
4672 | application/atom+xml: | ||
4673 | schema: | ||
4674 | $ref: '#/components/schemas/VideoCommentsForXML' | ||
4675 | examples: | ||
4676 | nightly: | ||
4677 | externalValue: https://peertube2.cpy.re/feeds/video-comments.atom?filter=local | ||
4678 | application/json: | ||
4679 | schema: | ||
4680 | type: object | ||
4681 | examples: | ||
4682 | nightly: | ||
4683 | externalValue: https://peertube2.cpy.re/feeds/video-comments.json?filter=local | ||
4684 | '400': | ||
4685 | x-summary: field inconsistencies | ||
4686 | description: > | ||
4687 | Arises when: | ||
4688 | - videoId filter is mixed with a channel filter | ||
4689 | '404': | ||
4690 | description: video, video channel or account not found | ||
4691 | '406': | ||
4692 | description: accept header unsupported | ||
4693 | |||
4694 | '/feeds/videos.{format}': | ||
4695 | get: | ||
4696 | tags: | ||
4697 | - Feeds | ||
4698 | summary: List videos | ||
4699 | operationId: getSyndicatedVideos | ||
4700 | parameters: | ||
4701 | - name: format | ||
4702 | in: path | ||
4703 | required: true | ||
4704 | description: 'format expected (we focus on making `rss` the most featureful ; it serves [Media RSS](https://www.rssboard.org/media-rss))' | ||
4705 | schema: | ||
4706 | type: string | ||
4707 | enum: | ||
4708 | - xml | ||
4709 | - rss | ||
4710 | - rss2 | ||
4711 | - atom | ||
4712 | - atom1 | ||
4713 | - json | ||
4714 | - json1 | ||
4715 | - name: accountId | ||
4716 | in: query | ||
4717 | description: 'limit listing to a specific account' | ||
4718 | schema: | ||
4719 | type: string | ||
4720 | - name: accountName | ||
4721 | in: query | ||
4722 | description: 'limit listing to a specific account' | ||
4723 | schema: | ||
4724 | type: string | ||
4725 | - name: videoChannelId | ||
4726 | in: query | ||
4727 | description: 'limit listing to a specific video channel' | ||
4728 | schema: | ||
4729 | type: string | ||
4730 | - name: videoChannelName | ||
4731 | in: query | ||
4732 | description: 'limit listing to a specific video channel' | ||
4733 | schema: | ||
4734 | type: string | ||
4735 | - $ref: '#/components/parameters/sort' | ||
4736 | - $ref: '#/components/parameters/nsfw' | ||
4737 | - $ref: '#/components/parameters/isLocal' | ||
4738 | - $ref: '#/components/parameters/include' | ||
4739 | - $ref: '#/components/parameters/privacyOneOf' | ||
4740 | - $ref: '#/components/parameters/hasHLSFiles' | ||
4741 | - $ref: '#/components/parameters/hasWebtorrentFiles' | ||
4742 | responses: | ||
4743 | '204': | ||
4744 | description: successful operation | ||
4745 | headers: | ||
4746 | Cache-Control: | ||
4747 | schema: | ||
4748 | type: string | ||
4749 | default: 'max-age=900' # 15 min cache | ||
4750 | content: | ||
4751 | application/xml: | ||
4752 | schema: | ||
4753 | $ref: '#/components/schemas/VideosForXML' | ||
4754 | examples: | ||
4755 | nightly: | ||
4756 | externalValue: https://peertube2.cpy.re/feeds/videos.xml?filter=local | ||
4757 | application/rss+xml: | ||
4758 | schema: | ||
4759 | $ref: '#/components/schemas/VideosForXML' | ||
4760 | examples: | ||
4761 | nightly: | ||
4762 | externalValue: https://peertube2.cpy.re/feeds/videos.rss?filter=local | ||
4763 | text/xml: | ||
4764 | schema: | ||
4765 | $ref: '#/components/schemas/VideosForXML' | ||
4766 | examples: | ||
4767 | nightly: | ||
4768 | externalValue: https://peertube2.cpy.re/feeds/videos.xml?filter=local | ||
4769 | application/atom+xml: | ||
4770 | schema: | ||
4771 | $ref: '#/components/schemas/VideosForXML' | ||
4772 | examples: | ||
4773 | nightly: | ||
4774 | externalValue: https://peertube2.cpy.re/feeds/videos.atom?filter=local | ||
4775 | application/json: | ||
4776 | schema: | ||
4777 | type: object | ||
4778 | examples: | ||
4779 | nightly: | ||
4780 | externalValue: https://peertube2.cpy.re/feeds/videos.json?filter=local | ||
4781 | '404': | ||
4782 | description: video channel or account not found | ||
4783 | '406': | ||
4784 | description: accept header unsupported | ||
4785 | |||
4786 | '/feeds/subscriptions.{format}': | ||
4787 | get: | ||
4788 | tags: | ||
4789 | - Feeds | ||
4790 | - Account | ||
4791 | summary: List videos of subscriptions tied to a token | ||
4792 | operationId: getSyndicatedSubscriptionVideos | ||
4793 | parameters: | ||
4794 | - name: format | ||
4795 | in: path | ||
4796 | required: true | ||
4797 | description: 'format expected (we focus on making `rss` the most featureful ; it serves [Media RSS](https://www.rssboard.org/media-rss))' | ||
4798 | schema: | ||
4799 | type: string | ||
4800 | enum: | ||
4801 | - xml | ||
4802 | - rss | ||
4803 | - rss2 | ||
4804 | - atom | ||
4805 | - atom1 | ||
4806 | - json | ||
4807 | - json1 | ||
4808 | - name: accountId | ||
4809 | in: query | ||
4810 | description: limit listing to a specific account | ||
4811 | schema: | ||
4812 | type: string | ||
4813 | required: true | ||
4814 | - name: token | ||
4815 | in: query | ||
4816 | description: private token allowing access | ||
4817 | schema: | ||
4818 | type: string | ||
4819 | required: true | ||
4820 | - $ref: '#/components/parameters/sort' | ||
4821 | - $ref: '#/components/parameters/nsfw' | ||
4822 | - $ref: '#/components/parameters/isLocal' | ||
4823 | - $ref: '#/components/parameters/include' | ||
4824 | - $ref: '#/components/parameters/privacyOneOf' | ||
4825 | - $ref: '#/components/parameters/hasHLSFiles' | ||
4826 | - $ref: '#/components/parameters/hasWebtorrentFiles' | ||
4827 | responses: | ||
4828 | '204': | ||
4829 | description: successful operation | ||
4830 | headers: | ||
4831 | Cache-Control: | ||
4832 | schema: | ||
4833 | type: string | ||
4834 | default: 'max-age=900' # 15 min cache | ||
4835 | content: | ||
4836 | application/xml: | ||
4837 | schema: | ||
4838 | $ref: '#/components/schemas/VideosForXML' | ||
4839 | application/rss+xml: | ||
4840 | schema: | ||
4841 | $ref: '#/components/schemas/VideosForXML' | ||
4842 | text/xml: | ||
4843 | schema: | ||
4844 | $ref: '#/components/schemas/VideosForXML' | ||
4845 | application/atom+xml: | ||
4846 | schema: | ||
4847 | $ref: '#/components/schemas/VideosForXML' | ||
4848 | application/json: | ||
4849 | schema: | ||
4850 | type: object | ||
4851 | '406': | ||
4852 | description: accept header unsupported | ||
4853 | |||
4854 | /plugins: | ||
4855 | get: | 4947 | get: |
4856 | tags: | 4948 | tags: |
4857 | - Plugins | 4949 | - Plugins |
@@ -4880,7 +4972,7 @@ paths: | |||
4880 | schema: | 4972 | schema: |
4881 | $ref: '#/components/schemas/PluginResponse' | 4973 | $ref: '#/components/schemas/PluginResponse' |
4882 | 4974 | ||
4883 | /plugins/available: | 4975 | /api/v1/plugins/available: |
4884 | get: | 4976 | get: |
4885 | tags: | 4977 | tags: |
4886 | - Plugins | 4978 | - Plugins |
@@ -4915,7 +5007,7 @@ paths: | |||
4915 | '503': | 5007 | '503': |
4916 | description: plugin index unavailable | 5008 | description: plugin index unavailable |
4917 | 5009 | ||
4918 | /plugins/install: | 5010 | /api/v1/plugins/install: |
4919 | post: | 5011 | post: |
4920 | tags: | 5012 | tags: |
4921 | - Plugins | 5013 | - Plugins |
@@ -4950,7 +5042,7 @@ paths: | |||
4950 | '400': | 5042 | '400': |
4951 | description: should have either `npmName` or `path` set | 5043 | description: should have either `npmName` or `path` set |
4952 | 5044 | ||
4953 | /plugins/update: | 5045 | /api/v1/plugins/update: |
4954 | post: | 5046 | post: |
4955 | tags: | 5047 | tags: |
4956 | - Plugins | 5048 | - Plugins |
@@ -4987,7 +5079,7 @@ paths: | |||
4987 | '404': | 5079 | '404': |
4988 | description: existing plugin not found | 5080 | description: existing plugin not found |
4989 | 5081 | ||
4990 | /plugins/uninstall: | 5082 | /api/v1/plugins/uninstall: |
4991 | post: | 5083 | post: |
4992 | tags: | 5084 | tags: |
4993 | - Plugins | 5085 | - Plugins |
@@ -5014,7 +5106,7 @@ paths: | |||
5014 | '404': | 5106 | '404': |
5015 | description: existing plugin not found | 5107 | description: existing plugin not found |
5016 | 5108 | ||
5017 | /plugins/{npmName}: | 5109 | /api/v1/plugins/{npmName}: |
5018 | get: | 5110 | get: |
5019 | tags: | 5111 | tags: |
5020 | - Plugins | 5112 | - Plugins |
@@ -5035,7 +5127,7 @@ paths: | |||
5035 | '404': | 5127 | '404': |
5036 | description: plugin not found | 5128 | description: plugin not found |
5037 | 5129 | ||
5038 | /plugins/{npmName}/settings: | 5130 | /api/v1/plugins/{npmName}/settings: |
5039 | put: | 5131 | put: |
5040 | tags: | 5132 | tags: |
5041 | - Plugins | 5133 | - Plugins |
@@ -5060,7 +5152,7 @@ paths: | |||
5060 | '404': | 5152 | '404': |
5061 | description: plugin not found | 5153 | description: plugin not found |
5062 | 5154 | ||
5063 | /plugins/{npmName}/public-settings: | 5155 | /api/v1/plugins/{npmName}/public-settings: |
5064 | get: | 5156 | get: |
5065 | tags: | 5157 | tags: |
5066 | - Plugins | 5158 | - Plugins |
@@ -5078,7 +5170,7 @@ paths: | |||
5078 | '404': | 5170 | '404': |
5079 | description: plugin not found | 5171 | description: plugin not found |
5080 | 5172 | ||
5081 | /plugins/{npmName}/registered-settings: | 5173 | /api/v1/plugins/{npmName}/registered-settings: |
5082 | get: | 5174 | get: |
5083 | tags: | 5175 | tags: |
5084 | - Plugins | 5176 | - Plugins |
@@ -5099,7 +5191,7 @@ paths: | |||
5099 | '404': | 5191 | '404': |
5100 | description: plugin not found | 5192 | description: plugin not found |
5101 | 5193 | ||
5102 | /metrics/playback: | 5194 | /api/v1/metrics/playback: |
5103 | post: | 5195 | post: |
5104 | summary: Create playback metrics | 5196 | summary: Create playback metrics |
5105 | description: These metrics are exposed by OpenTelemetry metrics exporter if enabled. | 5197 | description: These metrics are exposed by OpenTelemetry metrics exporter if enabled. |
@@ -5115,11 +5207,11 @@ paths: | |||
5115 | description: successful operation | 5207 | description: successful operation |
5116 | 5208 | ||
5117 | servers: | 5209 | servers: |
5118 | - url: 'https://peertube2.cpy.re/api/v1' | 5210 | - url: 'https://peertube2.cpy.re' |
5119 | description: Live Test Server (live data - latest nightly version) | 5211 | description: Live Test Server (live data - latest nightly version) |
5120 | - url: 'https://peertube3.cpy.re/api/v1' | 5212 | - url: 'https://peertube3.cpy.re' |
5121 | description: Live Test Server (live data - latest RC version) | 5213 | description: Live Test Server (live data - latest RC version) |
5122 | - url: 'https://peertube.cpy.re/api/v1' | 5214 | - url: 'https://peertube.cpy.re' |
5123 | description: Live Test Server (live data - stable version) | 5215 | description: Live Test Server (live data - stable version) |
5124 | components: | 5216 | components: |
5125 | parameters: | 5217 | parameters: |
@@ -5596,6 +5688,22 @@ components: | |||
5596 | - Group | 5688 | - Group |
5597 | - Service | 5689 | - Service |
5598 | - Organization | 5690 | - Organization |
5691 | staticFilename: | ||
5692 | name: filename | ||
5693 | in: path | ||
5694 | required: true | ||
5695 | description: Filename | ||
5696 | schema: | ||
5697 | type: string | ||
5698 | videoFileToken: | ||
5699 | name: videoFileToken | ||
5700 | in: query | ||
5701 | required: false | ||
5702 | description: Video file token [generated](#operation/requestVideoToken) by PeerTube so you don't need to provide an OAuth token in the request header. | ||
5703 | schema: | ||
5704 | type: string | ||
5705 | |||
5706 | |||
5599 | securitySchemes: | 5707 | securitySchemes: |
5600 | OAuth2: | 5708 | OAuth2: |
5601 | description: | | 5709 | description: | |
@@ -7349,6 +7457,16 @@ components: | |||
7349 | properties: | 7457 | properties: |
7350 | comment: | 7458 | comment: |
7351 | $ref: '#/components/schemas/VideoComment' | 7459 | $ref: '#/components/schemas/VideoComment' |
7460 | VideoTokenResponse: | ||
7461 | properties: | ||
7462 | files: | ||
7463 | type: object | ||
7464 | properties: | ||
7465 | token: | ||
7466 | type: string | ||
7467 | expires: | ||
7468 | type: string | ||
7469 | format: date-time | ||
7352 | VideoListResponse: | 7470 | VideoListResponse: |
7353 | properties: | 7471 | properties: |
7354 | total: | 7472 | total: |
diff --git a/support/nginx/peertube b/support/nginx/peertube index f6f754b58..cf200ba00 100644 --- a/support/nginx/peertube +++ b/support/nginx/peertube | |||
@@ -214,6 +214,10 @@ server { | |||
214 | try_files $uri @api; | 214 | try_files $uri @api; |
215 | } | 215 | } |
216 | 216 | ||
217 | location ~ ^/static/(webseed|streaming-playlists)/private/ { | ||
218 | try_files /dev/null @api; | ||
219 | } | ||
220 | |||
217 | # Bypass PeerTube for performance reasons. Optional. | 221 | # Bypass PeerTube for performance reasons. Optional. |
218 | location ~ ^/static/(webseed|redundancy|streaming-playlists)/ { | 222 | location ~ ^/static/(webseed|redundancy|streaming-playlists)/ { |
219 | limit_rate_after 5M; | 223 | limit_rate_after 5M; |
@@ -1921,11 +1921,6 @@ | |||
1921 | resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" | 1921 | resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" |
1922 | integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== | 1922 | integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== |
1923 | 1923 | ||
1924 | "@types/async-lock@^1.1.0": | ||
1925 | version "1.1.5" | ||
1926 | resolved "https://registry.yarnpkg.com/@types/async-lock/-/async-lock-1.1.5.tgz#a82f33e09aef451d6ded7bffae73f9d254723124" | ||
1927 | integrity sha512-A9ClUfmj6wwZMLRz0NaYzb98YH1exlHdf/cdDSKBfMQJnPOdO8xlEW0Eh2QsTTntGzOFWURcEjYElkZ1IY4GCQ== | ||
1928 | |||
1929 | "@types/bcrypt@^5.0.0": | 1924 | "@types/bcrypt@^5.0.0": |
1930 | version "5.0.0" | 1925 | version "5.0.0" |
1931 | resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-5.0.0.tgz#a835afa2882d165aff5690893db314eaa98b9f20" | 1926 | resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-5.0.0.tgz#a835afa2882d165aff5690893db314eaa98b9f20" |
@@ -2762,6 +2757,13 @@ async-lru@^1.1.1: | |||
2762 | dependencies: | 2757 | dependencies: |
2763 | lru "^3.1.0" | 2758 | lru "^3.1.0" |
2764 | 2759 | ||
2760 | async-mutex@^0.4.0: | ||
2761 | version "0.4.0" | ||
2762 | resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.0.tgz#ae8048cd4d04ace94347507504b3cf15e631c25f" | ||
2763 | integrity sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA== | ||
2764 | dependencies: | ||
2765 | tslib "^2.4.0" | ||
2766 | |||
2765 | async@3.2.3: | 2767 | async@3.2.3: |
2766 | version "3.2.3" | 2768 | version "3.2.3" |
2767 | resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" | 2769 | resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" |
@@ -8975,7 +8977,7 @@ tslib@^1.11.1, tslib@^1.8.1: | |||
8975 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" | 8977 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" |
8976 | integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== | 8978 | integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== |
8977 | 8979 | ||
8978 | tslib@^2.0.0, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.1: | 8980 | tslib@^2.0.0, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.1, tslib@^2.4.0: |
8979 | version "2.4.0" | 8981 | version "2.4.0" |
8980 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" | 8982 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" |
8981 | integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== | 8983 | integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== |