aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/shared/misc/utils.ts7
-rw-r--r--client/src/app/signup/signup.component.html3
-rw-r--r--client/src/app/signup/signup.component.ts34
-rw-r--r--client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts1
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html7
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss8
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts9
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html8
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss8
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts9
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-send.ts1
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.html9
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss10
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts9
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.html8
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.ts5
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts2
-rw-r--r--client/src/assets/player/images/tick-white.svg (renamed from client/src/assets/player/images/tick.svg)0
-rw-r--r--client/src/assets/player/peertube-videojs-plugin.ts5
-rw-r--r--client/src/sass/player/settings-menu.scss4
-rw-r--r--config/default.yaml5
-rw-r--r--config/production.yaml.example5
-rw-r--r--server/controllers/activitypub/client.ts16
-rw-r--r--server/controllers/api/videos/index.ts2
-rw-r--r--server/helpers/activitypub.ts14
-rw-r--r--server/helpers/requests.ts12
-rw-r--r--server/lib/activitypub/actor.ts9
-rw-r--r--server/lib/activitypub/videos.ts10
-rw-r--r--server/lib/job-queue/handlers/video-import.ts10
-rw-r--r--server/middlewares/cache.ts7
-rw-r--r--server/middlewares/oauth.ts16
-rw-r--r--server/middlewares/validators/videos/videos.ts54
-rw-r--r--server/models/activitypub/actor-follow.ts4
-rw-r--r--server/models/redundancy/video-redundancy.ts7
-rw-r--r--server/models/video/video.ts17
-rw-r--r--server/tests/api/check-params/user-subscriptions.ts6
-rw-r--r--server/tests/api/redundancy/redundancy.ts6
-rw-r--r--server/tests/api/server/follow-constraints.ts215
-rw-r--r--server/tests/api/server/index.ts1
-rw-r--r--support/doc/api/openapi.yaml295
40 files changed, 727 insertions, 131 deletions
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts
index 78be2e5dd..78e8e9682 100644
--- a/client/src/app/shared/misc/utils.ts
+++ b/client/src/app/shared/misc/utils.ts
@@ -124,6 +124,10 @@ function sortBy (obj: any[], key1: string, key2?: string) {
124 }) 124 })
125} 125}
126 126
127function scrollToTop () {
128 window.scroll(0, 0)
129}
130
127export { 131export {
128 sortBy, 132 sortBy,
129 durationToString, 133 durationToString,
@@ -135,5 +139,6 @@ export {
135 immutableAssign, 139 immutableAssign,
136 objectToFormData, 140 objectToFormData,
137 lineFeedToHtml, 141 lineFeedToHtml,
138 removeElementFromArray 142 removeElementFromArray,
143 scrollToTop
139} 144}
diff --git a/client/src/app/signup/signup.component.html b/client/src/app/signup/signup.component.html
index 531a97814..0207a166e 100644
--- a/client/src/app/signup/signup.component.html
+++ b/client/src/app/signup/signup.component.html
@@ -4,6 +4,7 @@
4 Create an account 4 Create an account
5 </div> 5 </div>
6 6
7 <div *ngIf="info" class="alert alert-info">{{ info }}</div>
7 <div *ngIf="error" class="alert alert-danger">{{ error }}</div> 8 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
8 9
9 <div class="d-flex justify-content-left flex-wrap"> 10 <div class="d-flex justify-content-left flex-wrap">
@@ -59,7 +60,7 @@
59 </div> 60 </div>
60 </div> 61 </div>
61 62
62 <input type="submit" i18n-value value="Signup" [disabled]="!form.valid"> 63 <input type="submit" i18n-value value="Signup" [disabled]="!form.valid || signupDone">
63 </form> 64 </form>
64 65
65 <div> 66 <div>
diff --git a/client/src/app/signup/signup.component.ts b/client/src/app/signup/signup.component.ts
index cf2657b85..3341d4e09 100644
--- a/client/src/app/signup/signup.component.ts
+++ b/client/src/app/signup/signup.component.ts
@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { NotificationsService } from 'angular2-notifications'
3import { UserCreate } from '../../../../shared' 3import { UserCreate } from '../../../../shared'
4import { FormReactive, UserService, UserValidatorsService } from '../shared' 4import { FormReactive, UserService, UserValidatorsService } from '../shared'
5import { RedirectService, ServerService } from '@app/core' 5import { AuthService, RedirectService, ServerService } from '@app/core'
6import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
8 8
@@ -12,10 +12,13 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val
12 styleUrls: [ './signup.component.scss' ] 12 styleUrls: [ './signup.component.scss' ]
13}) 13})
14export class SignupComponent extends FormReactive implements OnInit { 14export class SignupComponent extends FormReactive implements OnInit {
15 info: string = null
15 error: string = null 16 error: string = null
17 signupDone = false
16 18
17 constructor ( 19 constructor (
18 protected formValidatorService: FormValidatorService, 20 protected formValidatorService: FormValidatorService,
21 private authService: AuthService,
19 private userValidatorsService: UserValidatorsService, 22 private userValidatorsService: UserValidatorsService,
20 private notificationsService: NotificationsService, 23 private notificationsService: NotificationsService,
21 private userService: UserService, 24 private userService: UserService,
@@ -50,18 +53,27 @@ export class SignupComponent extends FormReactive implements OnInit {
50 53
51 this.userService.signup(userCreate).subscribe( 54 this.userService.signup(userCreate).subscribe(
52 () => { 55 () => {
56 this.signupDone = true
57
53 if (this.requiresEmailVerification) { 58 if (this.requiresEmailVerification) {
54 this.notificationsService.alert( 59 this.info = this.i18n('Welcome! Now please check your emails to verify your account and complete signup.')
55 this.i18n('Welcome'), 60 return
56 this.i18n('Please check your email to verify your account and complete signup.')
57 )
58 } else {
59 this.notificationsService.success(
60 this.i18n('Success'),
61 this.i18n('Registration for {{username}} complete.', { username: userCreate.username })
62 )
63 } 61 }
64 this.redirectService.redirectToHomepage() 62
63 // Auto login
64 this.authService.login(userCreate.username, userCreate.password)
65 .subscribe(
66 () => {
67 this.notificationsService.success(
68 this.i18n('Success'),
69 this.i18n('You are now logged in as {{username}}!', { username: userCreate.username })
70 )
71
72 this.redirectService.redirectToHomepage()
73 },
74
75 err => this.error = err.message
76 )
65 }, 77 },
66 78
67 err => this.error = err.message 79 err => this.error = err.message
diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
index 796fbe531..eaf819726 100644
--- a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
@@ -60,6 +60,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
60 hide () { 60 hide () {
61 this.closingModal = true 61 this.closingModal = true
62 this.openedModal.close() 62 this.openedModal.close()
63 this.form.reset()
63 } 64 }
64 65
65 isReplacingExistingCaption () { 66 isReplacingExistingCaption () {
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
index a933a64f0..11a81ad66 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
@@ -45,7 +45,12 @@
45 </div> 45 </div>
46</div> 46</div>
47 47
48<div *ngIf="hasImportedVideo" class="alert alert-info" i18n> 48<div *ngIf="error" class="alert alert-danger">
49 <div i18n>Sorry, but something went wrong</div>
50 {{ error }}
51</div>
52
53<div *ngIf="hasImportedVideo && !error" class="alert alert-info" i18n>
49 Congratulations, the video will be imported with BitTorrent! You can already add information about this video. 54 Congratulations, the video will be imported with BitTorrent! You can already add information about this video.
50</div> 55</div>
51 56
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss
index 262b0b68e..00626cd7b 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss
@@ -7,6 +7,14 @@ $width-size: 190px;
7 @include peertube-select-container($width-size); 7 @include peertube-select-container($width-size);
8} 8}
9 9
10.alert.alert-danger {
11 text-align: center;
12
13 & > div {
14 font-weight: $font-semibold;
15 }
16}
17
10.import-video-torrent { 18.import-video-torrent {
11 display: flex; 19 display: flex;
12 flex-direction: column; 20 flex-direction: column;
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
index e13c06ce9..13776ae36 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
@@ -12,6 +12,7 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
12import { FormValidatorService } from '@app/shared' 12import { FormValidatorService } from '@app/shared'
13import { VideoCaptionService } from '@app/shared/video-caption' 13import { VideoCaptionService } from '@app/shared/video-caption'
14import { VideoImportService } from '@app/shared/video-import' 14import { VideoImportService } from '@app/shared/video-import'
15import { scrollToTop } from '@app/shared/misc/utils'
15 16
16@Component({ 17@Component({
17 selector: 'my-video-import-torrent', 18 selector: 'my-video-import-torrent',
@@ -23,9 +24,9 @@ import { VideoImportService } from '@app/shared/video-import'
23}) 24})
24export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { 25export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate {
25 @Output() firstStepDone = new EventEmitter<string>() 26 @Output() firstStepDone = new EventEmitter<string>()
27 @Output() firstStepError = new EventEmitter<void>()
26 @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement> 28 @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement>
27 29
28 videoFileName: string
29 magnetUri = '' 30 magnetUri = ''
30 31
31 isImportingVideo = false 32 isImportingVideo = false
@@ -33,6 +34,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
33 isUpdatingVideo = false 34 isUpdatingVideo = false
34 35
35 video: VideoEdit 36 video: VideoEdit
37 error: string
36 38
37 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC 39 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
38 40
@@ -104,6 +106,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
104 err => { 106 err => {
105 this.loadingBar.complete() 107 this.loadingBar.complete()
106 this.isImportingVideo = false 108 this.isImportingVideo = false
109 this.firstStepError.emit()
107 this.notificationsService.error(this.i18n('Error'), err.message) 110 this.notificationsService.error(this.i18n('Error'), err.message)
108 } 111 }
109 ) 112 )
@@ -129,8 +132,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
129 }, 132 },
130 133
131 err => { 134 err => {
132 this.isUpdatingVideo = false 135 this.error = err.message
133 this.notificationsService.error(this.i18n('Error'), err.message) 136 scrollToTop()
134 console.error(err) 137 console.error(err)
135 } 138 }
136 ) 139 )
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
index 9f5fc6d22..533446672 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
@@ -37,7 +37,13 @@
37 </div> 37 </div>
38</div> 38</div>
39 39
40<div *ngIf="hasImportedVideo" class="alert alert-info" i18n> 40
41<div *ngIf="error" class="alert alert-danger">
42 <div i18n>Sorry, but something went wrong</div>
43 {{ error }}
44</div>
45
46<div *ngIf="!error && hasImportedVideo" class="alert alert-info" i18n>
41 Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video. 47 Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video.
42</div> 48</div>
43 49
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss
index 7c6deda1d..e907edc70 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss
@@ -7,6 +7,14 @@ $width-size: 190px;
7 @include peertube-select-container($width-size); 7 @include peertube-select-container($width-size);
8} 8}
9 9
10.alert.alert-danger {
11 text-align: center;
12
13 & > div {
14 font-weight: $font-semibold;
15 }
16}
17
10.import-video-url { 18.import-video-url {
11 display: flex; 19 display: flex;
12 flex-direction: column; 20 flex-direction: column;
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
index 031e557ed..9cdface75 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
@@ -12,6 +12,7 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
12import { FormValidatorService } from '@app/shared' 12import { FormValidatorService } from '@app/shared'
13import { VideoCaptionService } from '@app/shared/video-caption' 13import { VideoCaptionService } from '@app/shared/video-caption'
14import { VideoImportService } from '@app/shared/video-import' 14import { VideoImportService } from '@app/shared/video-import'
15import { scrollToTop } from '@app/shared/misc/utils'
15 16
16@Component({ 17@Component({
17 selector: 'my-video-import-url', 18 selector: 'my-video-import-url',
@@ -23,15 +24,16 @@ import { VideoImportService } from '@app/shared/video-import'
23}) 24})
24export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { 25export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate {
25 @Output() firstStepDone = new EventEmitter<string>() 26 @Output() firstStepDone = new EventEmitter<string>()
27 @Output() firstStepError = new EventEmitter<void>()
26 28
27 targetUrl = '' 29 targetUrl = ''
28 videoFileName: string
29 30
30 isImportingVideo = false 31 isImportingVideo = false
31 hasImportedVideo = false 32 hasImportedVideo = false
32 isUpdatingVideo = false 33 isUpdatingVideo = false
33 34
34 video: VideoEdit 35 video: VideoEdit
36 error: string
35 37
36 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC 38 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
37 39
@@ -96,6 +98,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
96 err => { 98 err => {
97 this.loadingBar.complete() 99 this.loadingBar.complete()
98 this.isImportingVideo = false 100 this.isImportingVideo = false
101 this.firstStepError.emit()
99 this.notificationsService.error(this.i18n('Error'), err.message) 102 this.notificationsService.error(this.i18n('Error'), err.message)
100 } 103 }
101 ) 104 )
@@ -121,8 +124,8 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
121 }, 124 },
122 125
123 err => { 126 err => {
124 this.isUpdatingVideo = false 127 this.error = err.message
125 this.notificationsService.error(this.i18n('Error'), err.message) 128 scrollToTop()
126 console.error(err) 129 console.error(err)
127 } 130 }
128 ) 131 )
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-send.ts b/client/src/app/videos/+video-edit/video-add-components/video-send.ts
index 1bf22e1a9..71d2544d8 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-send.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-send.ts
@@ -21,6 +21,7 @@ export abstract class VideoSend extends FormReactive implements OnInit {
21 firstStepChannelId = 0 21 firstStepChannelId = 0
22 22
23 abstract firstStepDone: EventEmitter<string> 23 abstract firstStepDone: EventEmitter<string>
24 abstract firstStepError: EventEmitter<void>
24 protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy 25 protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy
25 26
26 protected loadingBar: LoadingBarService 27 protected loadingBar: LoadingBarService
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
index fa57c8cb5..a09f54dfc 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
@@ -29,7 +29,7 @@
29 </div> 29 </div>
30</div> 30</div>
31 31
32<div *ngIf="isUploadingVideo" class="upload-progress-cancel"> 32<div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel">
33 <p-progressBar 33 <p-progressBar
34 [value]="videoUploadPercents" 34 [value]="videoUploadPercents"
35 [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }" 35 [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }"
@@ -37,6 +37,11 @@
37 <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" /> 37 <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" />
38</div> 38</div>
39 39
40<div *ngIf="error" class="alert alert-danger">
41 <div i18n>Sorry, but something went wrong</div>
42 {{ error }}
43</div>
44
40<!-- Hidden because we want to load the component --> 45<!-- Hidden because we want to load the component -->
41<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form"> 46<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
42 <my-video-edit 47 <my-video-edit
@@ -55,4 +60,4 @@
55 <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" /> 60 <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" />
56 </div> 61 </div>
57 </div> 62 </div>
58</form> \ No newline at end of file 63</form>
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
index dbae5230d..cf1725ef9 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
@@ -5,6 +5,14 @@
5 @include peertube-select-container(190px); 5 @include peertube-select-container(190px);
6} 6}
7 7
8.alert.alert-danger {
9 text-align: center;
10
11 & > div {
12 font-weight: $font-semibold;
13 }
14}
15
8.upload-video { 16.upload-video {
9 display: flex; 17 display: flex;
10 flex-direction: column; 18 flex-direction: column;
@@ -82,4 +90,4 @@
82 90
83 margin-left: 10px; 91 margin-left: 10px;
84 } 92 }
85} \ No newline at end of file 93}
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
index 8e2d0deaf..3fcb71ac3 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
@@ -14,6 +14,7 @@ import { VideoSend } from '@app/videos/+video-edit/video-add-components/video-se
14import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' 14import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
15import { FormValidatorService, UserService } from '@app/shared' 15import { FormValidatorService, UserService } from '@app/shared'
16import { VideoCaptionService } from '@app/shared/video-caption' 16import { VideoCaptionService } from '@app/shared/video-caption'
17import { scrollToTop } from '@app/shared/misc/utils'
17 18
18@Component({ 19@Component({
19 selector: 'my-video-upload', 20 selector: 'my-video-upload',
@@ -25,6 +26,7 @@ import { VideoCaptionService } from '@app/shared/video-caption'
25}) 26})
26export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { 27export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate {
27 @Output() firstStepDone = new EventEmitter<string>() 28 @Output() firstStepDone = new EventEmitter<string>()
29 @Output() firstStepError = new EventEmitter<void>()
28 @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement> 30 @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
29 31
30 // So that it can be accessed in the template 32 // So that it can be accessed in the template
@@ -43,6 +45,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
43 uuid: '' 45 uuid: ''
44 } 46 }
45 47
48 error: string
49
46 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC 50 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
47 51
48 constructor ( 52 constructor (
@@ -201,6 +205,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
201 this.isUploadingVideo = false 205 this.isUploadingVideo = false
202 this.videoUploadPercents = 0 206 this.videoUploadPercents = 0
203 this.videoUploadObservable = null 207 this.videoUploadObservable = null
208 this.firstStepError.emit()
204 this.notificationsService.error(this.i18n('Error'), err.message) 209 this.notificationsService.error(this.i18n('Error'), err.message)
205 } 210 }
206 ) 211 )
@@ -235,8 +240,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
235 }, 240 },
236 241
237 err => { 242 err => {
238 this.isUpdatingVideo = false 243 this.error = err.message
239 this.notificationsService.error(this.i18n('Error'), err.message) 244 scrollToTop()
240 console.error(err) 245 console.error(err)
241 } 246 }
242 ) 247 )
diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html
index e14e23aed..72a233b72 100644
--- a/client/src/app/videos/+video-edit/video-add.component.html
+++ b/client/src/app/videos/+video-edit/video-add.component.html
@@ -6,24 +6,24 @@
6 6
7 <ngb-tabset class="video-add-tabset root-tabset bootstrap" [ngClass]="{ 'hide-nav': secondStepType !== undefined }"> 7 <ngb-tabset class="video-add-tabset root-tabset bootstrap" [ngClass]="{ 'hide-nav': secondStepType !== undefined }">
8 8
9 <ngb-tab i18n-title title=""> 9 <ngb-tab>
10 <ng-template ngbTabTitle><span i18n>Upload a file</span></ng-template> 10 <ng-template ngbTabTitle><span i18n>Upload a file</span></ng-template>
11 <ng-template ngbTabContent> 11 <ng-template ngbTabContent>
12 <my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)"></my-video-upload> 12 <my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)" (firstStepError)="onError()"></my-video-upload>
13 </ng-template> 13 </ng-template>
14 </ngb-tab> 14 </ngb-tab>
15 15
16 <ngb-tab *ngIf="isVideoImportHttpEnabled()"> 16 <ngb-tab *ngIf="isVideoImportHttpEnabled()">
17 <ng-template ngbTabTitle><span i18n>Import with URL</span></ng-template> 17 <ng-template ngbTabTitle><span i18n>Import with URL</span></ng-template>
18 <ng-template ngbTabContent> 18 <ng-template ngbTabContent>
19 <my-video-import-url #videoImportUrl (firstStepDone)="onFirstStepDone('import-url', $event)"></my-video-import-url> 19 <my-video-import-url #videoImportUrl (firstStepDone)="onFirstStepDone('import-url', $event)" (firstStepError)="onError()"></my-video-import-url>
20 </ng-template> 20 </ng-template>
21 </ngb-tab> 21 </ngb-tab>
22 22
23 <ngb-tab *ngIf="isVideoImportTorrentEnabled()"> 23 <ngb-tab *ngIf="isVideoImportTorrentEnabled()">
24 <ng-template ngbTabTitle><span i18n>Import with torrent</span></ng-template> 24 <ng-template ngbTabTitle><span i18n>Import with torrent</span></ng-template>
25 <ng-template ngbTabContent> 25 <ng-template ngbTabContent>
26 <my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)"></my-video-import-torrent> 26 <my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)" (firstStepError)="onError()"></my-video-import-torrent>
27 </ng-template> 27 </ng-template>
28 </ngb-tab> 28 </ngb-tab>
29 </ngb-tabset> 29 </ngb-tabset>
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts
index 1a9247dbe..57a9d0ca7 100644
--- a/client/src/app/videos/+video-edit/video-add.component.ts
+++ b/client/src/app/videos/+video-edit/video-add.component.ts
@@ -27,6 +27,11 @@ export class VideoAddComponent implements CanComponentDeactivate {
27 this.videoName = videoName 27 this.videoName = videoName
28 } 28 }
29 29
30 onError () {
31 this.videoName = undefined
32 this.secondStepType = undefined
33 }
34
30 canDeactivate () { 35 canDeactivate () {
31 if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate() 36 if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate()
32 if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate() 37 if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate()
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 d0151ceb1..09ee96bdc 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -114,7 +114,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
114 ) 114 )
115 .pipe( 115 .pipe(
116 // If 401, the video is private or blacklisted so redirect to 404 116 // If 401, the video is private or blacklisted so redirect to 404
117 catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 404 ])) 117 catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ]))
118 ) 118 )
119 .subscribe(([ video, captionsResult ]) => { 119 .subscribe(([ video, captionsResult ]) => {
120 const startTime = this.route.snapshot.queryParams.start 120 const startTime = this.route.snapshot.queryParams.start
diff --git a/client/src/assets/player/images/tick.svg b/client/src/assets/player/images/tick-white.svg
index d329e6bfb..d329e6bfb 100644
--- a/client/src/assets/player/images/tick.svg
+++ b/client/src/assets/player/images/tick-white.svg
diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/peertube-videojs-plugin.ts
index 40da5f1f7..4fd5a9be2 100644
--- a/client/src/assets/player/peertube-videojs-plugin.ts
+++ b/client/src/assets/player/peertube-videojs-plugin.ts
@@ -111,6 +111,8 @@ class PeerTubePlugin extends Plugin {
111 const muted = getStoredMute() 111 const muted = getStoredMute()
112 if (muted !== undefined) this.player.muted(muted) 112 if (muted !== undefined) this.player.muted(muted)
113 113
114 this.player.duration(options.videoDuration)
115
114 this.initializePlayer() 116 this.initializePlayer()
115 this.runTorrentInfoScheduler() 117 this.runTorrentInfoScheduler()
116 this.runViewAdd() 118 this.runViewAdd()
@@ -302,6 +304,9 @@ class PeerTubePlugin extends Plugin {
302 304
303 this.flushVideoFile(previousVideoFile) 305 this.flushVideoFile(previousVideoFile)
304 306
307 // Update progress bar (just for the UI), do not wait rendering
308 if (options.seek) this.player.currentTime(options.seek)
309
305 const renderVideoOptions = { autoplay: false, controls: true } 310 const renderVideoOptions = { autoplay: false, controls: true }
306 renderVideo(torrent.files[ 0 ], this.playerElement, renderVideoOptions, (err, renderer) => { 311 renderVideo(torrent.files[ 0 ], this.playerElement, renderVideoOptions, (err, renderer) => {
307 this.renderer = renderer 312 this.renderer = renderer
diff --git a/client/src/sass/player/settings-menu.scss b/client/src/sass/player/settings-menu.scss
index d065e72fb..61965c85e 100644
--- a/client/src/sass/player/settings-menu.scss
+++ b/client/src/sass/player/settings-menu.scss
@@ -171,7 +171,7 @@ $setting-transition-easing: ease-out;
171 left: 8px; 171 left: 8px;
172 content: ' '; 172 content: ' ';
173 margin-top: 1px; 173 margin-top: 1px;
174 background-image: url('#{$assets-path}/player/images/tick.svg'); 174 background-image: url('#{$assets-path}/player/images/tick-white.svg');
175 } 175 }
176 } 176 }
177 } 177 }
@@ -197,4 +197,4 @@ $setting-transition-easing: ease-out;
197 } 197 }
198 } 198 }
199 } 199 }
200} \ No newline at end of file 200}
diff --git a/config/default.yaml b/config/default.yaml
index 0d7d948c2..257ec7ed1 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -58,7 +58,10 @@ log:
58 level: 'info' # debug/info/warning/error 58 level: 'info' # debug/info/warning/error
59 59
60search: 60search:
61 remote_uri: # Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance 61 # Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance
62 # If enabled, the associated group will be able to "escape" from the instance follows
63 # That means they will be able to follow channels, watch videos, list videos of non followed instances
64 remote_uri:
62 users: true 65 users: true
63 anonymous: false 66 anonymous: false
64 67
diff --git a/config/production.yaml.example b/config/production.yaml.example
index f9da8e0dd..ac15fc736 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -59,7 +59,10 @@ log:
59 level: 'info' # debug/info/warning/error 59 level: 'info' # debug/info/warning/error
60 60
61search: 61search:
62 remote_uri: # Add ability to search remote videos/actors by URI, that may not be federated with your instance 62 # Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance
63 # If enabled, the associated group will be able to "escape" from the instance follows
64 # That means they will be able to follow channels, watch videos, list videos of non followed instances
65 remote_uri:
63 users: true 66 users: true
64 anonymous: false 67 anonymous: false
65 68
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index ffbf1ba19..d9d385460 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -39,6 +39,7 @@ import {
39import { VideoCaptionModel } from '../../models/video/video-caption' 39import { VideoCaptionModel } from '../../models/video/video-caption'
40import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy' 40import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy'
41import { getServerActor } from '../../helpers/utils' 41import { getServerActor } from '../../helpers/utils'
42import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
42 43
43const activityPubClientRouter = express.Router() 44const activityPubClientRouter = express.Router()
44 45
@@ -164,6 +165,8 @@ function getAccountVideoRate (rateType: VideoRateType) {
164async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { 165async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
165 const video: VideoModel = res.locals.video 166 const video: VideoModel = res.locals.video
166 167
168 if (video.isOwned() === false) return res.redirect(video.url)
169
167 // We need captions to render AP object 170 // We need captions to render AP object
168 video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id) 171 video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id)
169 172
@@ -180,6 +183,9 @@ async function videoController (req: express.Request, res: express.Response, nex
180 183
181async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) { 184async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) {
182 const share = res.locals.videoShare as VideoShareModel 185 const share = res.locals.videoShare as VideoShareModel
186
187 if (share.Actor.isOwned() === false) return res.redirect(share.url)
188
183 const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined) 189 const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined)
184 190
185 return activityPubResponse(activityPubContextify(activity), res) 191 return activityPubResponse(activityPubContextify(activity), res)
@@ -252,6 +258,8 @@ async function videoChannelFollowingController (req: express.Request, res: expre
252async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) { 258async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) {
253 const videoComment: VideoCommentModel = res.locals.videoComment 259 const videoComment: VideoCommentModel = res.locals.videoComment
254 260
261 if (videoComment.isOwned() === false) return res.redirect(videoComment.url)
262
255 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined) 263 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
256 const isPublic = true // Comments are always public 264 const isPublic = true // Comments are always public
257 const audience = getAudience(videoComment.Account.Actor, isPublic) 265 const audience = getAudience(videoComment.Account.Actor, isPublic)
@@ -267,7 +275,9 @@ async function videoCommentController (req: express.Request, res: express.Respon
267} 275}
268 276
269async function videoRedundancyController (req: express.Request, res: express.Response) { 277async function videoRedundancyController (req: express.Request, res: express.Response) {
270 const videoRedundancy = res.locals.videoRedundancy 278 const videoRedundancy: VideoRedundancyModel = res.locals.videoRedundancy
279 if (videoRedundancy.isOwned() === false) return res.redirect(videoRedundancy.url)
280
271 const serverActor = await getServerActor() 281 const serverActor = await getServerActor()
272 282
273 const audience = getAudience(serverActor) 283 const audience = getAudience(serverActor)
@@ -288,7 +298,7 @@ async function actorFollowing (req: express.Request, actor: ActorModel) {
288 return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count) 298 return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count)
289 } 299 }
290 300
291 return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, handler, req.query.page) 301 return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.path, handler, req.query.page)
292} 302}
293 303
294async function actorFollowers (req: express.Request, actor: ActorModel) { 304async function actorFollowers (req: express.Request, actor: ActorModel) {
@@ -296,7 +306,7 @@ async function actorFollowers (req: express.Request, actor: ActorModel) {
296 return ActorFollowModel.listAcceptedFollowerUrlsForApi([ actor.id ], undefined, start, count) 306 return ActorFollowModel.listAcceptedFollowerUrlsForApi([ actor.id ], undefined, start, count)
297 } 307 }
298 308
299 return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, handler, req.query.page) 309 return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.path, handler, req.query.page)
300} 310}
301 311
302function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) { 312function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) {
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index e654bdd09..89fd0432f 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -31,6 +31,7 @@ import {
31 asyncMiddleware, 31 asyncMiddleware,
32 asyncRetryTransactionMiddleware, 32 asyncRetryTransactionMiddleware,
33 authenticate, 33 authenticate,
34 checkVideoFollowConstraints,
34 commonVideosFiltersValidator, 35 commonVideosFiltersValidator,
35 optionalAuthenticate, 36 optionalAuthenticate,
36 paginationValidator, 37 paginationValidator,
@@ -123,6 +124,7 @@ videosRouter.get('/:id/description',
123videosRouter.get('/:id', 124videosRouter.get('/:id',
124 optionalAuthenticate, 125 optionalAuthenticate,
125 asyncMiddleware(videosGetValidator), 126 asyncMiddleware(videosGetValidator),
127 asyncMiddleware(checkVideoFollowConstraints),
126 getVideo 128 getVideo
127) 129)
128videosRouter.post('/:id/views', 130videosRouter.post('/:id/views',
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index 4bf6e387d..bcbd9be59 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -57,16 +57,16 @@ function activityPubContextify <T> (data: T) {
57} 57}
58 58
59type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>> 59type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>>
60async function activityPubCollectionPagination (url: string, handler: ActivityPubCollectionPaginationHandler, page?: any) { 60async function activityPubCollectionPagination (baseUrl: string, handler: ActivityPubCollectionPaginationHandler, page?: any) {
61 if (!page || !validator.isInt(page)) { 61 if (!page || !validator.isInt(page)) {
62 // We just display the first page URL, we only need the total items 62 // We just display the first page URL, we only need the total items
63 const result = await handler(0, 1) 63 const result = await handler(0, 1)
64 64
65 return { 65 return {
66 id: url, 66 id: baseUrl,
67 type: 'OrderedCollection', 67 type: 'OrderedCollection',
68 totalItems: result.total, 68 totalItems: result.total,
69 first: url + '?page=1' 69 first: baseUrl + '?page=1'
70 } 70 }
71 } 71 }
72 72
@@ -81,19 +81,19 @@ async function activityPubCollectionPagination (url: string, handler: ActivityPu
81 81
82 // There are more results 82 // There are more results
83 if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) { 83 if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) {
84 next = url + '?page=' + (page + 1) 84 next = baseUrl + '?page=' + (page + 1)
85 } 85 }
86 86
87 if (page > 1) { 87 if (page > 1) {
88 prev = url + '?page=' + (page - 1) 88 prev = baseUrl + '?page=' + (page - 1)
89 } 89 }
90 90
91 return { 91 return {
92 id: url + '?page=' + page, 92 id: baseUrl + '?page=' + page,
93 type: 'OrderedCollectionPage', 93 type: 'OrderedCollectionPage',
94 prev, 94 prev,
95 next, 95 next,
96 partOf: url, 96 partOf: baseUrl,
97 orderedItems: result.data, 97 orderedItems: result.data,
98 totalItems: result.total 98 totalItems: result.total
99 } 99 }
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index 51facc9e0..805930a9f 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -2,6 +2,7 @@ import * as Bluebird from 'bluebird'
2import { createWriteStream } from 'fs-extra' 2import { createWriteStream } from 'fs-extra'
3import * as request from 'request' 3import * as request from 'request'
4import { ACTIVITY_PUB } from '../initializers' 4import { ACTIVITY_PUB } from '../initializers'
5import { processImage } from './image-utils'
5 6
6function doRequest <T> ( 7function doRequest <T> (
7 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } 8 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
@@ -27,9 +28,18 @@ function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.U
27 }) 28 })
28} 29}
29 30
31async function downloadImage (url: string, destPath: string, size: { width: number, height: number }) {
32 const tmpPath = destPath + '.tmp'
33
34 await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath)
35
36 await processImage({ path: tmpPath }, destPath, size)
37}
38
30// --------------------------------------------------------------------------- 39// ---------------------------------------------------------------------------
31 40
32export { 41export {
33 doRequest, 42 doRequest,
34 doRequestAndSaveToFile 43 doRequestAndSaveToFile,
44 downloadImage
35} 45}
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index b16a00669..218dbc6a7 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -11,9 +11,9 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp
11import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' 11import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
12import { logger } from '../../helpers/logger' 12import { logger } from '../../helpers/logger'
13import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' 13import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
14import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' 14import { doRequest, doRequestAndSaveToFile, downloadImage } from '../../helpers/requests'
15import { getUrlFromWebfinger } from '../../helpers/webfinger' 15import { getUrlFromWebfinger } from '../../helpers/webfinger'
16import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' 16import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, PREVIEWS_SIZE, sequelizeTypescript } from '../../initializers'
17import { AccountModel } from '../../models/account/account' 17import { AccountModel } from '../../models/account/account'
18import { ActorModel } from '../../models/activitypub/actor' 18import { ActorModel } from '../../models/activitypub/actor'
19import { AvatarModel } from '../../models/avatar/avatar' 19import { AvatarModel } from '../../models/avatar/avatar'
@@ -180,10 +180,7 @@ async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
180 const avatarName = uuidv4() + extension 180 const avatarName = uuidv4() + extension
181 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) 181 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
182 182
183 await doRequestAndSaveToFile({ 183 await downloadImage(actorJSON.icon.url, destPath, AVATARS_SIZE)
184 method: 'GET',
185 uri: actorJSON.icon.url
186 }, destPath)
187 184
188 return avatarName 185 return avatarName
189 } 186 }
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 5bd03c8c6..80de92f24 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -10,8 +10,8 @@ import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validat
10import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 10import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
11import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 11import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
12import { logger } from '../../helpers/logger' 12import { logger } from '../../helpers/logger'
13import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' 13import { doRequest, downloadImage } from '../../helpers/requests'
14import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers' 14import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_MIMETYPE_EXT } from '../../initializers'
15import { ActorModel } from '../../models/activitypub/actor' 15import { ActorModel } from '../../models/activitypub/actor'
16import { TagModel } from '../../models/video/tag' 16import { TagModel } from '../../models/video/tag'
17import { VideoModel } from '../../models/video/video' 17import { VideoModel } from '../../models/video/video'
@@ -97,11 +97,7 @@ function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject)
97 const thumbnailName = video.getThumbnailName() 97 const thumbnailName = video.getThumbnailName()
98 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) 98 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
99 99
100 const options = { 100 return downloadImage(icon.url, thumbnailPath, THUMBNAILS_SIZE)
101 method: 'GET',
102 uri: icon.url
103 }
104 return doRequestAndSaveToFile(options, thumbnailPath)
105} 101}
106 102
107function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { 103function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index e3f2a276c..4de901c0c 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -6,8 +6,8 @@ import { VideoImportState } from '../../../../shared/models/videos'
6import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' 6import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
7import { extname, join } from 'path' 7import { extname, join } from 'path'
8import { VideoFileModel } from '../../../models/video/video-file' 8import { VideoFileModel } from '../../../models/video/video-file'
9import { CONFIG, sequelizeTypescript, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' 9import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers'
10import { doRequestAndSaveToFile } from '../../../helpers/requests' 10import { doRequestAndSaveToFile, downloadImage } from '../../../helpers/requests'
11import { VideoState } from '../../../../shared' 11import { VideoState } from '../../../../shared'
12import { JobQueue } from '../index' 12import { JobQueue } from '../index'
13import { federateVideoIfNeeded } from '../../activitypub' 13import { federateVideoIfNeeded } from '../../activitypub'
@@ -133,7 +133,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
133 videoId: videoImport.videoId 133 videoId: videoImport.videoId
134 } 134 }
135 videoFile = new VideoFileModel(videoFileData) 135 videoFile = new VideoFileModel(videoFileData)
136 // Import if the import fails, to clean files 136 // To clean files if the import fails
137 videoImport.Video.VideoFiles = [ videoFile ] 137 videoImport.Video.VideoFiles = [ videoFile ]
138 138
139 // Move file 139 // Move file
@@ -145,7 +145,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
145 if (options.downloadThumbnail) { 145 if (options.downloadThumbnail) {
146 if (options.thumbnailUrl) { 146 if (options.thumbnailUrl) {
147 const destThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName()) 147 const destThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName())
148 await doRequestAndSaveToFile({ method: 'GET', uri: options.thumbnailUrl }, destThumbnailPath) 148 await downloadImage(options.thumbnailUrl, destThumbnailPath, THUMBNAILS_SIZE)
149 } else { 149 } else {
150 await videoImport.Video.createThumbnail(videoFile) 150 await videoImport.Video.createThumbnail(videoFile)
151 } 151 }
@@ -157,7 +157,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
157 if (options.downloadPreview) { 157 if (options.downloadPreview) {
158 if (options.thumbnailUrl) { 158 if (options.thumbnailUrl) {
159 const destPreviewPath = join(CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName()) 159 const destPreviewPath = join(CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName())
160 await doRequestAndSaveToFile({ method: 'GET', uri: options.thumbnailUrl }, destPreviewPath) 160 await downloadImage(options.thumbnailUrl, destPreviewPath, PREVIEWS_SIZE)
161 } else { 161 } else {
162 await videoImport.Video.createPreview(videoFile) 162 await videoImport.Video.createPreview(videoFile)
163 } 163 }
diff --git a/server/middlewares/cache.ts b/server/middlewares/cache.ts
index 1e00fc731..8ffe75700 100644
--- a/server/middlewares/cache.ts
+++ b/server/middlewares/cache.ts
@@ -19,6 +19,7 @@ function cacheRoute (lifetimeArg: string | number) {
19 logger.debug('No cached results for route %s.', req.originalUrl) 19 logger.debug('No cached results for route %s.', req.originalUrl)
20 20
21 const sendSave = res.send.bind(res) 21 const sendSave = res.send.bind(res)
22 const redirectSave = res.redirect.bind(res)
22 23
23 res.send = (body) => { 24 res.send = (body) => {
24 if (res.statusCode >= 200 && res.statusCode < 400) { 25 if (res.statusCode >= 200 && res.statusCode < 400) {
@@ -38,6 +39,12 @@ function cacheRoute (lifetimeArg: string | number) {
38 return sendSave(body) 39 return sendSave(body)
39 } 40 }
40 41
42 res.redirect = url => {
43 done()
44
45 return redirectSave(url)
46 }
47
41 return next() 48 return next()
42 } 49 }
43 50
diff --git a/server/middlewares/oauth.ts b/server/middlewares/oauth.ts
index 5233b66bd..8c1df2c3e 100644
--- a/server/middlewares/oauth.ts
+++ b/server/middlewares/oauth.ts
@@ -28,9 +28,24 @@ function authenticate (req: express.Request, res: express.Response, next: expres
28 }) 28 })
29} 29}
30 30
31function authenticatePromiseIfNeeded (req: express.Request, res: express.Response) {
32 return new Promise(resolve => {
33 // Already authenticated? (or tried to)
34 if (res.locals.oauth && res.locals.oauth.token.User) return resolve()
35
36 if (res.locals.authenticated === false) return res.sendStatus(401)
37
38 authenticate(req, res, () => {
39 return resolve()
40 })
41 })
42}
43
31function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) { 44function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) {
32 if (req.header('authorization')) return authenticate(req, res, next) 45 if (req.header('authorization')) return authenticate(req, res, next)
33 46
47 res.locals.authenticated = false
48
34 return next() 49 return next()
35} 50}
36 51
@@ -53,6 +68,7 @@ function token (req: express.Request, res: express.Response, next: express.NextF
53 68
54export { 69export {
55 authenticate, 70 authenticate,
71 authenticatePromiseIfNeeded,
56 optionalAuthenticate, 72 optionalAuthenticate,
57 token 73 token
58} 74}
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 656d161d8..051a19e16 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -31,8 +31,8 @@ import {
31} from '../../../helpers/custom-validators/videos' 31} from '../../../helpers/custom-validators/videos'
32import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' 32import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
33import { logger } from '../../../helpers/logger' 33import { logger } from '../../../helpers/logger'
34import { CONSTRAINTS_FIELDS } from '../../../initializers' 34import { CONFIG, CONSTRAINTS_FIELDS } from '../../../initializers'
35import { authenticate } from '../../oauth' 35import { authenticatePromiseIfNeeded } from '../../oauth'
36import { areValidationErrors } from '../utils' 36import { areValidationErrors } from '../utils'
37import { cleanUpReqFiles } from '../../../helpers/express-utils' 37import { cleanUpReqFiles } from '../../../helpers/express-utils'
38import { VideoModel } from '../../../models/video/video' 38import { VideoModel } from '../../../models/video/video'
@@ -43,6 +43,7 @@ import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ow
43import { AccountModel } from '../../../models/account/account' 43import { AccountModel } from '../../../models/account/account'
44import { VideoFetchType } from '../../../helpers/video' 44import { VideoFetchType } from '../../../helpers/video'
45import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' 45import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
46import { getServerActor } from '../../../helpers/utils'
46 47
47const videosAddValidator = getCommonVideoAttributes().concat([ 48const videosAddValidator = getCommonVideoAttributes().concat([
48 body('videofile') 49 body('videofile')
@@ -127,6 +128,31 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([
127 } 128 }
128]) 129])
129 130
131async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
132 const video: VideoModel = res.locals.video
133
134 // Anybody can watch local videos
135 if (video.isOwned() === true) return next()
136
137 // Logged user
138 if (res.locals.oauth) {
139 // Users can search or watch remote videos
140 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
141 }
142
143 // Anybody can search or watch remote videos
144 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
145
146 // Check our instance follows an actor that shared this video
147 const serverActor = await getServerActor()
148 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
149
150 return res.status(403)
151 .json({
152 error: 'Cannot get this video regarding follow constraints.'
153 })
154}
155
130const videosCustomGetValidator = (fetchType: VideoFetchType) => { 156const videosCustomGetValidator = (fetchType: VideoFetchType) => {
131 return [ 157 return [
132 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 158 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
@@ -141,17 +167,20 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => {
141 167
142 // Video private or blacklisted 168 // Video private or blacklisted
143 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) { 169 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
144 return authenticate(req, res, () => { 170 await authenticatePromiseIfNeeded(req, res)
145 const user: UserModel = res.locals.oauth.token.User
146 171
147 // Only the owner or a user that have blacklist rights can see the video 172 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : null
148 if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
149 return res.status(403)
150 .json({ error: 'Cannot get this private or blacklisted video.' })
151 }
152 173
153 return next() 174 // Only the owner or a user that have blacklist rights can see the video
154 }) 175 if (
176 !user ||
177 (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
178 ) {
179 return res.status(403)
180 .json({ error: 'Cannot get this private or blacklisted video.' })
181 }
182
183 return next()
155 } 184 }
156 185
157 // Video is public, anyone can access it 186 // Video is public, anyone can access it
@@ -376,6 +405,7 @@ export {
376 videosAddValidator, 405 videosAddValidator,
377 videosUpdateValidator, 406 videosUpdateValidator,
378 videosGetValidator, 407 videosGetValidator,
408 checkVideoFollowConstraints,
379 videosCustomGetValidator, 409 videosCustomGetValidator,
380 videosRemoveValidator, 410 videosRemoveValidator,
381 411
@@ -393,6 +423,8 @@ export {
393function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) { 423function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
394 if (req.body.scheduleUpdate) { 424 if (req.body.scheduleUpdate) {
395 if (!req.body.scheduleUpdate.updateAt) { 425 if (!req.body.scheduleUpdate.updateAt) {
426 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
427
396 res.status(400) 428 res.status(400)
397 .json({ error: 'Schedule update at is mandatory.' }) 429 .json({ error: 'Schedule update at is mandatory.' })
398 430
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index 3373355ef..0a6935083 100644
--- a/server/models/activitypub/actor-follow.ts
+++ b/server/models/activitypub/actor-follow.ts
@@ -509,12 +509,12 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
509 tasks.push(ActorFollowModel.sequelize.query(query, options)) 509 tasks.push(ActorFollowModel.sequelize.query(query, options))
510 } 510 }
511 511
512 const [ followers, [ { total } ] ] = await Promise.all(tasks) 512 const [ followers, [ dataTotal ] ] = await Promise.all(tasks)
513 const urls: string[] = followers.map(f => f.url) 513 const urls: string[] = followers.map(f => f.url)
514 514
515 return { 515 return {
516 data: urls, 516 data: urls,
517 total: parseInt(total, 10) 517 total: dataTotal ? parseInt(dataTotal.total, 10) : 0
518 } 518 }
519 } 519 }
520 520
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 35e0cd3b1..9de4356b4 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -117,8 +117,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
117 117
118 @BeforeDestroy 118 @BeforeDestroy
119 static async removeFile (instance: VideoRedundancyModel) { 119 static async removeFile (instance: VideoRedundancyModel) {
120 // Not us 120 if (!instance.isOwned()) return
121 if (!instance.strategy) return
122 121
123 const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId) 122 const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId)
124 123
@@ -404,6 +403,10 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
404 })) 403 }))
405 } 404 }
406 405
406 isOwned () {
407 return !!this.strategy
408 }
409
407 toActivityPubObject (): CacheFileObject { 410 toActivityPubObject (): CacheFileObject {
408 return { 411 return {
409 id: this.url, 412 id: this.url,
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 6c183933b..1e68b380c 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1253,6 +1253,23 @@ export class VideoModel extends Model<VideoModel> {
1253 }) 1253 })
1254 } 1254 }
1255 1255
1256 static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
1257 // Instances only share videos
1258 const query = 'SELECT 1 FROM "videoShare" ' +
1259 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
1260 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' +
1261 'LIMIT 1'
1262
1263 const options = {
1264 type: Sequelize.QueryTypes.SELECT,
1265 bind: { followerActorId, videoId },
1266 raw: true
1267 }
1268
1269 return VideoModel.sequelize.query(query, options)
1270 .then(results => results.length === 1)
1271 }
1272
1256 // threshold corresponds to how many video the field should have to be returned 1273 // threshold corresponds to how many video the field should have to be returned
1257 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { 1274 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
1258 const serverActor = await getServerActor() 1275 const serverActor = await getServerActor()
diff --git a/server/tests/api/check-params/user-subscriptions.ts b/server/tests/api/check-params/user-subscriptions.ts
index 2cf5a2415..8a9ced7c1 100644
--- a/server/tests/api/check-params/user-subscriptions.ts
+++ b/server/tests/api/check-params/user-subscriptions.ts
@@ -14,11 +14,13 @@ import {
14 setAccessTokensToServers, 14 setAccessTokensToServers,
15 userLogin 15 userLogin
16} from '../../../../shared/utils' 16} from '../../../../shared/utils'
17
17import { 18import {
18 checkBadCountPagination, 19 checkBadCountPagination,
19 checkBadSortPagination, 20 checkBadSortPagination,
20 checkBadStartPagination 21 checkBadStartPagination
21} from '../../../../shared/utils/requests/check-api-params' 22} from '../../../../shared/utils/requests/check-api-params'
23import { waitJobs } from '../../../../shared/utils/server/jobs'
22 24
23describe('Test user subscriptions API validators', function () { 25describe('Test user subscriptions API validators', function () {
24 const path = '/api/v1/users/me/subscriptions' 26 const path = '/api/v1/users/me/subscriptions'
@@ -145,6 +147,8 @@ describe('Test user subscriptions API validators', function () {
145 }) 147 })
146 148
147 it('Should succeed with the correct parameters', async function () { 149 it('Should succeed with the correct parameters', async function () {
150 this.timeout(20000)
151
148 await makePostBodyRequest({ 152 await makePostBodyRequest({
149 url: server.url, 153 url: server.url,
150 path, 154 path,
@@ -152,6 +156,8 @@ describe('Test user subscriptions API validators', function () {
152 fields: { uri: 'user1_channel@localhost:9001' }, 156 fields: { uri: 'user1_channel@localhost:9001' },
153 statusCodeExpected: 204 157 statusCodeExpected: 204
154 }) 158 })
159
160 await waitJobs([ server ])
155 }) 161 })
156 }) 162 })
157 163
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts
index 663e31ead..2bc1b60ce 100644
--- a/server/tests/api/redundancy/redundancy.ts
+++ b/server/tests/api/redundancy/redundancy.ts
@@ -17,9 +17,10 @@ import {
17 viewVideo, 17 viewVideo,
18 wait, 18 wait,
19 waitUntilLog, 19 waitUntilLog,
20 checkVideoFilesWereRemoved, removeVideo 20 checkVideoFilesWereRemoved, removeVideo, getVideoWithToken
21} from '../../../../shared/utils' 21} from '../../../../shared/utils'
22import { waitJobs } from '../../../../shared/utils/server/jobs' 22import { waitJobs } from '../../../../shared/utils/server/jobs'
23
23import * as magnetUtil from 'magnet-uri' 24import * as magnetUtil from 'magnet-uri'
24import { updateRedundancy } from '../../../../shared/utils/server/redundancy' 25import { updateRedundancy } from '../../../../shared/utils/server/redundancy'
25import { ActorFollow } from '../../../../shared/models/actors' 26import { ActorFollow } from '../../../../shared/models/actors'
@@ -93,7 +94,8 @@ async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: str
93 94
94 for (const server of servers) { 95 for (const server of servers) {
95 { 96 {
96 const res = await getVideo(server.url, videoUUID) 97 // With token to avoid issues with video follow constraints
98 const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
97 99
98 const video: VideoDetails = res.body 100 const video: VideoDetails = res.body
99 for (const f of video.files) { 101 for (const f of video.files) {
diff --git a/server/tests/api/server/follow-constraints.ts b/server/tests/api/server/follow-constraints.ts
new file mode 100644
index 000000000..3135fc568
--- /dev/null
+++ b/server/tests/api/server/follow-constraints.ts
@@ -0,0 +1,215 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import { doubleFollow, getAccountVideos, getVideo, getVideoChannelVideos, getVideoWithToken } from '../../utils'
6import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils/index'
7import { unfollow } from '../../utils/server/follows'
8import { userLogin } from '../../utils/users/login'
9import { createUser } from '../../utils/users/users'
10
11const expect = chai.expect
12
13describe('Test follow constraints', function () {
14 let servers: ServerInfo[] = []
15 let video1UUID: string
16 let video2UUID: string
17 let userAccessToken: string
18
19 before(async function () {
20 this.timeout(30000)
21
22 servers = await flushAndRunMultipleServers(2)
23
24 // Get the access tokens
25 await setAccessTokensToServers(servers)
26
27 {
28 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video server 1' })
29 video1UUID = res.body.video.uuid
30 }
31 {
32 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video server 2' })
33 video2UUID = res.body.video.uuid
34 }
35
36 const user = {
37 username: 'user1',
38 password: 'super_password'
39 }
40 await createUser(servers[0].url, servers[0].accessToken, user.username, user.password)
41 userAccessToken = await userLogin(servers[0], user)
42
43 await doubleFollow(servers[0], servers[1])
44 })
45
46 describe('With a followed instance', function () {
47
48 describe('With an unlogged user', function () {
49
50 it('Should get the local video', async function () {
51 await getVideo(servers[0].url, video1UUID, 200)
52 })
53
54 it('Should get the remote video', async function () {
55 await getVideo(servers[0].url, video2UUID, 200)
56 })
57
58 it('Should list local account videos', async function () {
59 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9001', 0, 5)
60
61 expect(res.body.total).to.equal(1)
62 expect(res.body.data).to.have.lengthOf(1)
63 })
64
65 it('Should list remote account videos', async function () {
66 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9002', 0, 5)
67
68 expect(res.body.total).to.equal(1)
69 expect(res.body.data).to.have.lengthOf(1)
70 })
71
72 it('Should list local channel videos', async function () {
73 const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9001', 0, 5)
74
75 expect(res.body.total).to.equal(1)
76 expect(res.body.data).to.have.lengthOf(1)
77 })
78
79 it('Should list remote channel videos', async function () {
80 const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9002', 0, 5)
81
82 expect(res.body.total).to.equal(1)
83 expect(res.body.data).to.have.lengthOf(1)
84 })
85 })
86
87 describe('With a logged user', function () {
88 it('Should get the local video', async function () {
89 await getVideoWithToken(servers[0].url, userAccessToken, video1UUID, 200)
90 })
91
92 it('Should get the remote video', async function () {
93 await getVideoWithToken(servers[0].url, userAccessToken, video2UUID, 200)
94 })
95
96 it('Should list local account videos', async function () {
97 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9001', 0, 5)
98
99 expect(res.body.total).to.equal(1)
100 expect(res.body.data).to.have.lengthOf(1)
101 })
102
103 it('Should list remote account videos', async function () {
104 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9002', 0, 5)
105
106 expect(res.body.total).to.equal(1)
107 expect(res.body.data).to.have.lengthOf(1)
108 })
109
110 it('Should list local channel videos', async function () {
111 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9001', 0, 5)
112
113 expect(res.body.total).to.equal(1)
114 expect(res.body.data).to.have.lengthOf(1)
115 })
116
117 it('Should list remote channel videos', async function () {
118 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9002', 0, 5)
119
120 expect(res.body.total).to.equal(1)
121 expect(res.body.data).to.have.lengthOf(1)
122 })
123 })
124 })
125
126 describe('With a non followed instance', function () {
127
128 before(async function () {
129 this.timeout(30000)
130
131 await unfollow(servers[0].url, servers[0].accessToken, servers[1])
132 })
133
134 describe('With an unlogged user', function () {
135
136 it('Should get the local video', async function () {
137 await getVideo(servers[0].url, video1UUID, 200)
138 })
139
140 it('Should not get the remote video', async function () {
141 await getVideo(servers[0].url, video2UUID, 403)
142 })
143
144 it('Should list local account videos', async function () {
145 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9001', 0, 5)
146
147 expect(res.body.total).to.equal(1)
148 expect(res.body.data).to.have.lengthOf(1)
149 })
150
151 it('Should not list remote account videos', async function () {
152 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9002', 0, 5)
153
154 expect(res.body.total).to.equal(0)
155 expect(res.body.data).to.have.lengthOf(0)
156 })
157
158 it('Should list local channel videos', async function () {
159 const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9001', 0, 5)
160
161 expect(res.body.total).to.equal(1)
162 expect(res.body.data).to.have.lengthOf(1)
163 })
164
165 it('Should not list remote channel videos', async function () {
166 const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9002', 0, 5)
167
168 expect(res.body.total).to.equal(0)
169 expect(res.body.data).to.have.lengthOf(0)
170 })
171 })
172
173 describe('With a logged user', function () {
174 it('Should get the local video', async function () {
175 await getVideoWithToken(servers[0].url, userAccessToken, video1UUID, 200)
176 })
177
178 it('Should get the remote video', async function () {
179 await getVideoWithToken(servers[0].url, userAccessToken, video2UUID, 200)
180 })
181
182 it('Should list local account videos', async function () {
183 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9001', 0, 5)
184
185 expect(res.body.total).to.equal(1)
186 expect(res.body.data).to.have.lengthOf(1)
187 })
188
189 it('Should list remote account videos', async function () {
190 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9002', 0, 5)
191
192 expect(res.body.total).to.equal(1)
193 expect(res.body.data).to.have.lengthOf(1)
194 })
195
196 it('Should list local channel videos', async function () {
197 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9001', 0, 5)
198
199 expect(res.body.total).to.equal(1)
200 expect(res.body.data).to.have.lengthOf(1)
201 })
202
203 it('Should list remote channel videos', async function () {
204 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9002', 0, 5)
205
206 expect(res.body.total).to.equal(1)
207 expect(res.body.data).to.have.lengthOf(1)
208 })
209 })
210 })
211
212 after(async function () {
213 killallServers(servers)
214 })
215})
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts
index 78ab7e18b..6afcab1f9 100644
--- a/server/tests/api/server/index.ts
+++ b/server/tests/api/server/index.ts
@@ -1,5 +1,6 @@
1import './config' 1import './config'
2import './email' 2import './email'
3import './follow-constraints'
3import './follows' 4import './follows'
4import './handle-down' 5import './handle-down'
5import './jobs' 6import './jobs'
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 666e48a41..af829cc62 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -10,27 +10,41 @@ info:
10 url: 'https://github.com/Chocobozzz/PeerTube/blob/master/LICENSE' 10 url: 'https://github.com/Chocobozzz/PeerTube/blob/master/LICENSE'
11 x-logo: 11 x-logo:
12 url: 'https://joinpeertube.org/img/brand.png' 12 url: 'https://joinpeertube.org/img/brand.png'
13 altText: PeerTube Project Homepage
13 description: | 14 description: |
14 # Introduction 15 # Introduction
15 The PeerTube API is built on HTTP(S). Our API is RESTful. It has predictable 16 The PeerTube API is built on HTTP(S). Our API is RESTful. It has predictable
16 resource URLs. It returns HTTP response codes to indicate errors. It also 17 resource URLs. It returns HTTP response codes to indicate errors. It also
17 accepts and returns JSON in the HTTP body. You can use your favorite 18 accepts and returns JSON in the HTTP body. You can use your favorite
18 HTTP/REST library for your programming language to use PeerTube. No official 19 HTTP/REST library for your programming language to use PeerTube. No official
19 SDK is currently provided. 20 SDK is currently provided, but the spec API is fully compatible with
21 [openapi-generator](https://github.com/OpenAPITools/openapi-generator/wiki/API-client-generator-HOWTO)
22 which generates a client SDK in the language of your choice.
20 23
21 # Authentication 24 # Authentication
22 When you sign up for an account, you are given the possibility to generate 25 When you sign up for an account, you are given the possibility to generate
23 sessions, and authenticate using this session token. One session token can 26 sessions, and authenticate using this session token. One session token can
24 currently be used at a time. 27 currently be used at a time.
28
29 # Errors
30 The API uses standard HTTP status codes to indicate the success or failure
31 of the API call. The body of the response will be JSON in the following
32 format.
33
34 ```
35 {
36 "code": "unauthorized_request", // example inner error code
37 "error": "Token is invalid." // example exposed error message
38 }
39 ```
40externalDocs:
41 url: https://docs.joinpeertube.org/api.html
25tags: 42tags:
26 - name: Accounts 43 - name: Accounts
27 description: > 44 description: >
28 Using some features of PeerTube require authentication, for which Accounts 45 Using some features of PeerTube require authentication, for which Accounts
29
30 provide different levels of permission as well as associated user 46 provide different levels of permission as well as associated user
31 information. 47 information. Accounts also encompass remote accounts discovered across the federation.
32
33 Accounts also encompass remote accounts discovered across the federation.
34 - name: Config 48 - name: Config
35 description: > 49 description: >
36 Each server exposes public information regarding supported videos and 50 Each server exposes public information regarding supported videos and
@@ -42,23 +56,15 @@ tags:
42 - name: Job 56 - name: Job
43 description: > 57 description: >
44 Jobs are long-running tasks enqueued and processed by the instance 58 Jobs are long-running tasks enqueued and processed by the instance
45 itself. 59 itself. No additional worker registration is currently available.
46 60 - name: Server Following
47 No additional worker registration is currently available.
48 - name: ServerFollowing
49 description: > 61 description: >
50 Managing servers which the instance interacts with is crucial to the 62 Managing servers which the instance interacts with is crucial to the
51 concept 63 concept of federation in PeerTube and external video indexation. The PeerTube
52 64 server then deals with inter-server ActivityPub operations and propagates
53 of federation in PeerTube and external video indexation. The PeerTube
54 server
55
56 then deals with inter-server ActivityPub operations and propagates
57
58 information across its social graph by posting activities to actors' inbox 65 information across its social graph by posting activities to actors' inbox
59
60 endpoints. 66 endpoints.
61 - name: VideoAbuse 67 - name: Video Abuse
62 description: | 68 description: |
63 Video abuses deal with reports of local or remote videos alike. 69 Video abuses deal with reports of local or remote videos alike.
64 - name: Video 70 - name: Video
@@ -70,16 +76,51 @@ tags:
70 Videos from other instances federated by the instance (that is, instances 76 Videos from other instances federated by the instance (that is, instances
71 followed by the instance) can be found via keywords and other criteria of 77 followed by the instance) can be found via keywords and other criteria of
72 the advanced search. 78 the advanced search.
73 - name: VideoComment 79 - name: Video Comment
74 description: > 80 description: >
75 Operations dealing with comments to a video. Comments are organized in 81 Operations dealing with comments to a video. Comments are organized in
76 threads. 82 threads.
77 - name: VideoChannel 83 - name: Video Channel
78 description: > 84 description: >
79 Operations dealing with creation, modification and video listing of a 85 Operations dealing with creation, modification and video listing of a
80 user's 86 user's channels.
81 87 - name: Video Blacklist
82 channels. 88 description: >
89 Operations dealing with blacklisting videos (removing them from view and
90 preventing interactions).
91 - name: Video Rate
92 description: >
93 Voting for a video.
94x-tagGroups:
95 - name: Accounts
96 tags:
97 - Accounts
98 - User
99 - name: Videos
100 tags:
101 - Video
102 - Video Channel
103 - Video Comment
104 - Video Abuse
105 - Video Following
106 - Video Rate
107 - name: Moderation
108 tags:
109 - Video Abuse
110 - Video Blacklist
111 - name: Public Instance Information
112 tags:
113 - Config
114 - Server Following
115 - name: Notifications
116 tags:
117 - Feeds
118 - name: Jobs
119 tags:
120 - Job
121 - name: Search
122 tags:
123 - Search
83paths: 124paths:
84 '/accounts/{name}': 125 '/accounts/{name}':
85 get: 126 get:
@@ -126,6 +167,37 @@ paths:
126 source: | 167 source: |
127 # pip install httpie 168 # pip install httpie
128 http -b GET https://peertube2.cpy.re/api/v1/accounts/{name}/videos 169 http -b GET https://peertube2.cpy.re/api/v1/accounts/{name}/videos
170 - lang: Ruby
171 source: |
172 require 'uri'
173 require 'net/http'
174
175 url = URI("https://peertube2.cpy.re/api/v1/accounts/{name}/videos")
176
177 http = Net::HTTP.new(url.host, url.port)
178 http.use_ssl = true
179 http.verify_mode = OpenSSL::SSL::VERIFY_NONE
180
181 request = Net::HTTP::Post.new(url)
182 request["content-type"] = 'application/json'
183 response = http.request(request)
184 puts response.read_body
185 - lang: Python
186 source: |
187 import http.client
188
189 conn = http.client.HTTPSConnection("https://peertube2.cpy.re/api/v1")
190
191 headers = {
192 'content-type': "application/json"
193 }
194
195 conn.request("POST", "/accounts/{name}/videos", None, headers)
196
197 res = conn.getresponse()
198 data = res.read()
199
200 print(data.decode("utf-8"))
129 /accounts: 201 /accounts:
130 get: 202 get:
131 tags: 203 tags:
@@ -144,7 +216,7 @@ paths:
144 get: 216 get:
145 tags: 217 tags:
146 - Config 218 - Config
147 summary: Get the configuration of the server 219 summary: Get the public configuration of the server
148 responses: 220 responses:
149 '200': 221 '200':
150 description: successful operation 222 description: successful operation
@@ -152,6 +224,45 @@ paths:
152 application/json: 224 application/json:
153 schema: 225 schema:
154 $ref: '#/components/schemas/ServerConfig' 226 $ref: '#/components/schemas/ServerConfig'
227 /config/about:
228 get:
229 summary: Get the instance about page content
230 tags:
231 - Config
232 responses:
233 '200':
234 description: successful operation
235 /config/custom:
236 get:
237 summary: Get the runtime configuration of the server
238 tags:
239 - Config
240 security:
241 - OAuth2:
242 - admin
243 responses:
244 '200':
245 description: successful operation
246 put:
247 summary: Set the runtime configuration of the server
248 tags:
249 - Config
250 security:
251 - OAuth2:
252 - admin
253 responses:
254 '200':
255 description: successful operation
256 delete:
257 summary: Delete the runtime configuration of the server
258 tags:
259 - Config
260 security:
261 - OAuth2:
262 - admin
263 responses:
264 '200':
265 description: successful operation
155 '/feeds/videos.{format}': 266 '/feeds/videos.{format}':
156 get: 267 get:
157 summary: >- 268 summary: >-
@@ -223,7 +334,7 @@ paths:
223 - OAuth2: 334 - OAuth2:
224 - admin 335 - admin
225 tags: 336 tags:
226 - ServerFollowing 337 - Server Following
227 summary: Unfollow a server by hostname 338 summary: Unfollow a server by hostname
228 parameters: 339 parameters:
229 - name: host 340 - name: host
@@ -238,7 +349,7 @@ paths:
238 /server/followers: 349 /server/followers:
239 get: 350 get:
240 tags: 351 tags:
241 - ServerFollowing 352 - Server Following
242 summary: Get followers of the server 353 summary: Get followers of the server
243 parameters: 354 parameters:
244 - $ref: '#/components/parameters/start' 355 - $ref: '#/components/parameters/start'
@@ -256,7 +367,7 @@ paths:
256 /server/following: 367 /server/following:
257 get: 368 get:
258 tags: 369 tags:
259 - ServerFollowing 370 - Server Following
260 summary: Get servers followed by the server 371 summary: Get servers followed by the server
261 parameters: 372 parameters:
262 - $ref: '#/components/parameters/start' 373 - $ref: '#/components/parameters/start'
@@ -276,7 +387,7 @@ paths:
276 - OAuth2: 387 - OAuth2:
277 - admin 388 - admin
278 tags: 389 tags:
279 - ServerFollowing 390 - Server Following
280 summary: Follow a server 391 summary: Follow a server
281 responses: 392 responses:
282 '204': 393 '204':
@@ -701,6 +812,85 @@ paths:
701 responses: 812 responses:
702 '204': 813 '204':
703 $ref: '#/paths/~1users~1me/put/responses/204' 814 $ref: '#/paths/~1users~1me/put/responses/204'
815 '/videos/{id}/watching':
816 put:
817 summary: Indicate progress of in watching the video by its id for a user
818 tags:
819 - Video
820 security:
821 - OAuth2: []
822 parameters:
823 - $ref: '#/components/parameters/id2'
824 requestBody:
825 content:
826 application/json:
827 schema:
828 $ref: '#/components/schemas/UserWatchingVideo'
829 required: true
830 responses:
831 '204':
832 $ref: '#/paths/~1users~1me/put/responses/204'
833 /videos/ownership:
834 get:
835 summary: Get list of video ownership changes requests
836 tags:
837 - Video
838 security:
839 - OAuth2: []
840 parameters:
841 - $ref: '#/components/parameters/id2'
842 responses:
843 '200':
844 description: successful operation
845 '/videos/ownership/{id}/accept':
846 post:
847 summary: Refuse ownership change request for video by its id
848 tags:
849 - Video
850 security:
851 - OAuth2: []
852 parameters:
853 - $ref: '#/components/parameters/id2'
854 responses:
855 '204':
856 $ref: '#/paths/~1users~1me/put/responses/204'
857 '/videos/ownership/{id}/refuse':
858 post:
859 summary: Accept ownership change request for video by its id
860 tags:
861 - Video
862 security:
863 - OAuth2: []
864 parameters:
865 - $ref: '#/components/parameters/id2'
866 responses:
867 '204':
868 $ref: '#/paths/~1users~1me/put/responses/204'
869 '/videos/{id}/give-ownership':
870 post:
871 summary: Request change of ownership for a video you own, by its id
872 tags:
873 - Video
874 security:
875 - OAuth2: []
876 parameters:
877 - $ref: '#/components/parameters/id2'
878 requestBody:
879 required: true
880 content:
881 application/x-www-form-urlencoded:
882 schema:
883 type: object
884 properties:
885 username:
886 type: string
887 required:
888 - username
889 responses:
890 '204':
891 $ref: '#/paths/~1users~1me/put/responses/204'
892 '400':
893 description: 'Changing video ownership to a remote account is not supported yet'
704 /videos/upload: 894 /videos/upload:
705 post: 895 post:
706 summary: Upload a video file with its metadata 896 summary: Upload a video file with its metadata
@@ -771,7 +961,6 @@ paths:
771 - videofile 961 - videofile
772 - channelId 962 - channelId
773 - name 963 - name
774 - privacy
775 x-code-samples: 964 x-code-samples:
776 - lang: Shell 965 - lang: Shell
777 source: | 966 source: |
@@ -781,7 +970,6 @@ paths:
781 PASSWORD="<your_password>" 970 PASSWORD="<your_password>"
782 FILE_PATH="<your_file_path>" 971 FILE_PATH="<your_file_path>"
783 CHANNEL_ID="<your_channel_id>" 972 CHANNEL_ID="<your_channel_id>"
784 PRIVACY="1" # public: 1, unlisted: 2, private: 3
785 NAME="<video_name>" 973 NAME="<video_name>"
786 974
787 API_PATH="https://peertube2.cpy.re/api/v1" 975 API_PATH="https://peertube2.cpy.re/api/v1"
@@ -798,7 +986,6 @@ paths:
798 videofile@$FILE_PATH \ 986 videofile@$FILE_PATH \
799 channelId=$CHANNEL_ID \ 987 channelId=$CHANNEL_ID \
800 name=$NAME \ 988 name=$NAME \
801 privacy=$PRIVACY \
802 "Authorization:Bearer $token" 989 "Authorization:Bearer $token"
803 /videos/abuse: 990 /videos/abuse:
804 get: 991 get:
@@ -806,7 +993,7 @@ paths:
806 security: 993 security:
807 - OAuth2: [] 994 - OAuth2: []
808 tags: 995 tags:
809 - VideoAbuse 996 - Video Abuse
810 parameters: 997 parameters:
811 - $ref: '#/components/parameters/start' 998 - $ref: '#/components/parameters/start'
812 - $ref: '#/components/parameters/count' 999 - $ref: '#/components/parameters/count'
@@ -826,7 +1013,7 @@ paths:
826 security: 1013 security:
827 - OAuth2: [] 1014 - OAuth2: []
828 tags: 1015 tags:
829 - VideoAbuse 1016 - Video Abuse
830 parameters: 1017 parameters:
831 - $ref: '#/components/parameters/id2' 1018 - $ref: '#/components/parameters/id2'
832 responses: 1019 responses:
@@ -840,7 +1027,7 @@ paths:
840 - admin 1027 - admin
841 - moderator 1028 - moderator
842 tags: 1029 tags:
843 - VideoBlacklist 1030 - Video Blacklist
844 parameters: 1031 parameters:
845 - $ref: '#/components/parameters/id2' 1032 - $ref: '#/components/parameters/id2'
846 responses: 1033 responses:
@@ -853,7 +1040,7 @@ paths:
853 - admin 1040 - admin
854 - moderator 1041 - moderator
855 tags: 1042 tags:
856 - VideoBlacklist 1043 - Video Blacklist
857 parameters: 1044 parameters:
858 - $ref: '#/components/parameters/id2' 1045 - $ref: '#/components/parameters/id2'
859 responses: 1046 responses:
@@ -867,7 +1054,7 @@ paths:
867 - admin 1054 - admin
868 - moderator 1055 - moderator
869 tags: 1056 tags:
870 - VideoBlacklist 1057 - Video Blacklist
871 parameters: 1058 parameters:
872 - $ref: '#/components/parameters/start' 1059 - $ref: '#/components/parameters/start'
873 - $ref: '#/components/parameters/count' 1060 - $ref: '#/components/parameters/count'
@@ -885,7 +1072,7 @@ paths:
885 get: 1072 get:
886 summary: Get list of video channels 1073 summary: Get list of video channels
887 tags: 1074 tags:
888 - VideoChannel 1075 - Video Channel
889 parameters: 1076 parameters:
890 - $ref: '#/components/parameters/start' 1077 - $ref: '#/components/parameters/start'
891 - $ref: '#/components/parameters/count' 1078 - $ref: '#/components/parameters/count'
@@ -904,7 +1091,7 @@ paths:
904 security: 1091 security:
905 - OAuth2: [] 1092 - OAuth2: []
906 tags: 1093 tags:
907 - VideoChannel 1094 - Video Channel
908 responses: 1095 responses:
909 '204': 1096 '204':
910 $ref: '#/paths/~1users~1me/put/responses/204' 1097 $ref: '#/paths/~1users~1me/put/responses/204'
@@ -914,7 +1101,7 @@ paths:
914 get: 1101 get:
915 summary: Get a video channel by its id 1102 summary: Get a video channel by its id
916 tags: 1103 tags:
917 - VideoChannel 1104 - Video Channel
918 parameters: 1105 parameters:
919 - $ref: '#/components/parameters/id3' 1106 - $ref: '#/components/parameters/id3'
920 responses: 1107 responses:
@@ -929,7 +1116,7 @@ paths:
929 security: 1116 security:
930 - OAuth2: [] 1117 - OAuth2: []
931 tags: 1118 tags:
932 - VideoChannel 1119 - Video Channel
933 parameters: 1120 parameters:
934 - $ref: '#/components/parameters/id3' 1121 - $ref: '#/components/parameters/id3'
935 responses: 1122 responses:
@@ -942,7 +1129,7 @@ paths:
942 security: 1129 security:
943 - OAuth2: [] 1130 - OAuth2: []
944 tags: 1131 tags:
945 - VideoChannel 1132 - Video Channel
946 parameters: 1133 parameters:
947 - $ref: '#/components/parameters/id3' 1134 - $ref: '#/components/parameters/id3'
948 responses: 1135 responses:
@@ -952,7 +1139,7 @@ paths:
952 get: 1139 get:
953 summary: Get videos of a video channel by its id 1140 summary: Get videos of a video channel by its id
954 tags: 1141 tags:
955 - VideoChannel 1142 - Video Channel
956 parameters: 1143 parameters:
957 - $ref: '#/components/parameters/id3' 1144 - $ref: '#/components/parameters/id3'
958 responses: 1145 responses:
@@ -966,7 +1153,7 @@ paths:
966 get: 1153 get:
967 summary: Get video channels of an account by its name 1154 summary: Get video channels of an account by its name
968 tags: 1155 tags:
969 - VideoChannel 1156 - Video Channel
970 parameters: 1157 parameters:
971 - $ref: '#/components/parameters/name' 1158 - $ref: '#/components/parameters/name'
972 responses: 1159 responses:
@@ -982,7 +1169,7 @@ paths:
982 get: 1169 get:
983 summary: Get the comment threads of a video by its id 1170 summary: Get the comment threads of a video by its id
984 tags: 1171 tags:
985 - VideoComment 1172 - Video Comment
986 parameters: 1173 parameters:
987 - $ref: '#/components/parameters/id2' 1174 - $ref: '#/components/parameters/id2'
988 - $ref: '#/components/parameters/start' 1175 - $ref: '#/components/parameters/start'
@@ -1000,7 +1187,7 @@ paths:
1000 security: 1187 security:
1001 - OAuth2: [] 1188 - OAuth2: []
1002 tags: 1189 tags:
1003 - VideoComment 1190 - Video Comment
1004 parameters: 1191 parameters:
1005 - $ref: '#/components/parameters/id2' 1192 - $ref: '#/components/parameters/id2'
1006 responses: 1193 responses:
@@ -1014,7 +1201,7 @@ paths:
1014 get: 1201 get:
1015 summary: 'Get the comment thread by its id, of a video by its id' 1202 summary: 'Get the comment thread by its id, of a video by its id'
1016 tags: 1203 tags:
1017 - VideoComment 1204 - Video Comment
1018 parameters: 1205 parameters:
1019 - $ref: '#/components/parameters/id2' 1206 - $ref: '#/components/parameters/id2'
1020 - name: threadId 1207 - name: threadId
@@ -1036,7 +1223,7 @@ paths:
1036 security: 1223 security:
1037 - OAuth2: [] 1224 - OAuth2: []
1038 tags: 1225 tags:
1039 - VideoComment 1226 - Video Comment
1040 parameters: 1227 parameters:
1041 - $ref: '#/components/parameters/id2' 1228 - $ref: '#/components/parameters/id2'
1042 - $ref: '#/components/parameters/commentId' 1229 - $ref: '#/components/parameters/commentId'
@@ -1052,7 +1239,7 @@ paths:
1052 security: 1239 security:
1053 - OAuth2: [] 1240 - OAuth2: []
1054 tags: 1241 tags:
1055 - VideoComment 1242 - Video Comment
1056 parameters: 1243 parameters:
1057 - $ref: '#/components/parameters/id2' 1244 - $ref: '#/components/parameters/id2'
1058 - $ref: '#/components/parameters/commentId' 1245 - $ref: '#/components/parameters/commentId'
@@ -1065,7 +1252,7 @@ paths:
1065 security: 1252 security:
1066 - OAuth2: [] 1253 - OAuth2: []
1067 tags: 1254 tags:
1068 - VideoRate 1255 - Video Rate
1069 parameters: 1256 parameters:
1070 - $ref: '#/components/parameters/id2' 1257 - $ref: '#/components/parameters/id2'
1071 responses: 1258 responses:
@@ -1096,8 +1283,12 @@ paths:
1096 items: 1283 items:
1097 $ref: '#/components/schemas/Video' 1284 $ref: '#/components/schemas/Video'
1098servers: 1285servers:
1286 - url: 'https://peertube.cpy.re/api/v1'
1287 description: Live Test Server (live data - stable version)
1099 - url: 'https://peertube2.cpy.re/api/v1' 1288 - url: 'https://peertube2.cpy.re/api/v1'
1100 description: Live Server 1289 description: Live Test Server (live data - bleeding edge version)
1290 - url: 'https://peertube3.cpy.re/api/v1'
1291 description: Live Test Server (live data - bleeding edge version)
1101components: 1292components:
1102 parameters: 1293 parameters:
1103 start: 1294 start:
@@ -1417,6 +1608,10 @@ components:
1417 type: array 1608 type: array
1418 items: 1609 items:
1419 $ref: '#/components/schemas/VideoChannel' 1610 $ref: '#/components/schemas/VideoChannel'
1611 UserWatchingVideo:
1612 properties:
1613 currentTime:
1614 type: number
1420 ServerConfig: 1615 ServerConfig:
1421 properties: 1616 properties:
1422 signup: 1617 signup: