diff options
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 | ||
127 | function scrollToTop () { | ||
128 | window.scroll(0, 0) | ||
129 | } | ||
130 | |||
127 | export { | 131 | export { |
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' | |||
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { NotificationsService } from 'angular2-notifications' |
3 | import { UserCreate } from '../../../../shared' | 3 | import { UserCreate } from '../../../../shared' |
4 | import { FormReactive, UserService, UserValidatorsService } from '../shared' | 4 | import { FormReactive, UserService, UserValidatorsService } from '../shared' |
5 | import { RedirectService, ServerService } from '@app/core' | 5 | import { AuthService, RedirectService, ServerService } from '@app/core' |
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
7 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 7 | import { 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 | }) |
14 | export class SignupComponent extends FormReactive implements OnInit { | 14 | export 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' | |||
12 | import { FormValidatorService } from '@app/shared' | 12 | import { FormValidatorService } from '@app/shared' |
13 | import { VideoCaptionService } from '@app/shared/video-caption' | 13 | import { VideoCaptionService } from '@app/shared/video-caption' |
14 | import { VideoImportService } from '@app/shared/video-import' | 14 | import { VideoImportService } from '@app/shared/video-import' |
15 | import { 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 | }) |
24 | export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 25 | export 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' | |||
12 | import { FormValidatorService } from '@app/shared' | 12 | import { FormValidatorService } from '@app/shared' |
13 | import { VideoCaptionService } from '@app/shared/video-caption' | 13 | import { VideoCaptionService } from '@app/shared/video-caption' |
14 | import { VideoImportService } from '@app/shared/video-import' | 14 | import { VideoImportService } from '@app/shared/video-import' |
15 | import { 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 | }) |
24 | export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 25 | export 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 | |||
14 | import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' | 14 | import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' |
15 | import { FormValidatorService, UserService } from '@app/shared' | 15 | import { FormValidatorService, UserService } from '@app/shared' |
16 | import { VideoCaptionService } from '@app/shared/video-caption' | 16 | import { VideoCaptionService } from '@app/shared/video-caption' |
17 | import { 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 | }) |
26 | export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { | 27 | export 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 | ||
60 | search: | 60 | search: |
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 | ||
61 | search: | 61 | search: |
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 { | |||
39 | import { VideoCaptionModel } from '../../models/video/video-caption' | 39 | import { VideoCaptionModel } from '../../models/video/video-caption' |
40 | import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy' | 40 | import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy' |
41 | import { getServerActor } from '../../helpers/utils' | 41 | import { getServerActor } from '../../helpers/utils' |
42 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | ||
42 | 43 | ||
43 | const activityPubClientRouter = express.Router() | 44 | const activityPubClientRouter = express.Router() |
44 | 45 | ||
@@ -164,6 +165,8 @@ function getAccountVideoRate (rateType: VideoRateType) { | |||
164 | async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { | 165 | async 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 | ||
181 | async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) { | 184 | async 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 | |||
252 | async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) { | 258 | async 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 | ||
269 | async function videoRedundancyController (req: express.Request, res: express.Response) { | 277 | async 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 | ||
294 | async function actorFollowers (req: express.Request, actor: ActorModel) { | 304 | async 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 | ||
302 | function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) { | 312 | function 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', | |||
123 | videosRouter.get('/:id', | 124 | videosRouter.get('/:id', |
124 | optionalAuthenticate, | 125 | optionalAuthenticate, |
125 | asyncMiddleware(videosGetValidator), | 126 | asyncMiddleware(videosGetValidator), |
127 | asyncMiddleware(checkVideoFollowConstraints), | ||
126 | getVideo | 128 | getVideo |
127 | ) | 129 | ) |
128 | videosRouter.post('/:id/views', | 130 | videosRouter.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 | ||
59 | type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>> | 59 | type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>> |
60 | async function activityPubCollectionPagination (url: string, handler: ActivityPubCollectionPaginationHandler, page?: any) { | 60 | async 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' | |||
2 | import { createWriteStream } from 'fs-extra' | 2 | import { createWriteStream } from 'fs-extra' |
3 | import * as request from 'request' | 3 | import * as request from 'request' |
4 | import { ACTIVITY_PUB } from '../initializers' | 4 | import { ACTIVITY_PUB } from '../initializers' |
5 | import { processImage } from './image-utils' | ||
5 | 6 | ||
6 | function doRequest <T> ( | 7 | function 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 | ||
31 | async 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 | ||
32 | export { | 41 | export { |
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 | |||
11 | import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' | 11 | import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' |
12 | import { logger } from '../../helpers/logger' | 12 | import { logger } from '../../helpers/logger' |
13 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' | 13 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' |
14 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | 14 | import { doRequest, doRequestAndSaveToFile, downloadImage } from '../../helpers/requests' |
15 | import { getUrlFromWebfinger } from '../../helpers/webfinger' | 15 | import { getUrlFromWebfinger } from '../../helpers/webfinger' |
16 | import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' | 16 | import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, PREVIEWS_SIZE, sequelizeTypescript } from '../../initializers' |
17 | import { AccountModel } from '../../models/account/account' | 17 | import { AccountModel } from '../../models/account/account' |
18 | import { ActorModel } from '../../models/activitypub/actor' | 18 | import { ActorModel } from '../../models/activitypub/actor' |
19 | import { AvatarModel } from '../../models/avatar/avatar' | 19 | import { 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 | |||
10 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 10 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
11 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' | 11 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' |
12 | import { logger } from '../../helpers/logger' | 12 | import { logger } from '../../helpers/logger' |
13 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | 13 | import { doRequest, downloadImage } from '../../helpers/requests' |
14 | import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers' | 14 | import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_MIMETYPE_EXT } from '../../initializers' |
15 | import { ActorModel } from '../../models/activitypub/actor' | 15 | import { ActorModel } from '../../models/activitypub/actor' |
16 | import { TagModel } from '../../models/video/tag' | 16 | import { TagModel } from '../../models/video/tag' |
17 | import { VideoModel } from '../../models/video/video' | 17 | import { 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 | ||
107 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { | 103 | function 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' | |||
6 | import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' | 6 | import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' |
7 | import { extname, join } from 'path' | 7 | import { extname, join } from 'path' |
8 | import { VideoFileModel } from '../../../models/video/video-file' | 8 | import { VideoFileModel } from '../../../models/video/video-file' |
9 | import { CONFIG, sequelizeTypescript, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' | 9 | import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' |
10 | import { doRequestAndSaveToFile } from '../../../helpers/requests' | 10 | import { doRequestAndSaveToFile, downloadImage } from '../../../helpers/requests' |
11 | import { VideoState } from '../../../../shared' | 11 | import { VideoState } from '../../../../shared' |
12 | import { JobQueue } from '../index' | 12 | import { JobQueue } from '../index' |
13 | import { federateVideoIfNeeded } from '../../activitypub' | 13 | import { 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 | ||
31 | function 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 | |||
31 | function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) { | 44 | function 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 | ||
54 | export { | 69 | export { |
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' |
32 | import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' | 32 | import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' |
33 | import { logger } from '../../../helpers/logger' | 33 | import { logger } from '../../../helpers/logger' |
34 | import { CONSTRAINTS_FIELDS } from '../../../initializers' | 34 | import { CONFIG, CONSTRAINTS_FIELDS } from '../../../initializers' |
35 | import { authenticate } from '../../oauth' | 35 | import { authenticatePromiseIfNeeded } from '../../oauth' |
36 | import { areValidationErrors } from '../utils' | 36 | import { areValidationErrors } from '../utils' |
37 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 37 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
38 | import { VideoModel } from '../../../models/video/video' | 38 | import { VideoModel } from '../../../models/video/video' |
@@ -43,6 +43,7 @@ import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ow | |||
43 | import { AccountModel } from '../../../models/account/account' | 43 | import { AccountModel } from '../../../models/account/account' |
44 | import { VideoFetchType } from '../../../helpers/video' | 44 | import { VideoFetchType } from '../../../helpers/video' |
45 | import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' | 45 | import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' |
46 | import { getServerActor } from '../../../helpers/utils' | ||
46 | 47 | ||
47 | const videosAddValidator = getCommonVideoAttributes().concat([ | 48 | const videosAddValidator = getCommonVideoAttributes().concat([ |
48 | body('videofile') | 49 | body('videofile') |
@@ -127,6 +128,31 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([ | |||
127 | } | 128 | } |
128 | ]) | 129 | ]) |
129 | 130 | ||
131 | async 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 | |||
130 | const videosCustomGetValidator = (fetchType: VideoFetchType) => { | 156 | const 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 { | |||
393 | function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) { | 423 | function 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 | |||
17 | import { | 18 | import { |
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' |
23 | import { waitJobs } from '../../../../shared/utils/server/jobs' | ||
22 | 24 | ||
23 | describe('Test user subscriptions API validators', function () { | 25 | describe('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' |
22 | import { waitJobs } from '../../../../shared/utils/server/jobs' | 22 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
23 | |||
23 | import * as magnetUtil from 'magnet-uri' | 24 | import * as magnetUtil from 'magnet-uri' |
24 | import { updateRedundancy } from '../../../../shared/utils/server/redundancy' | 25 | import { updateRedundancy } from '../../../../shared/utils/server/redundancy' |
25 | import { ActorFollow } from '../../../../shared/models/actors' | 26 | import { 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 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { doubleFollow, getAccountVideos, getVideo, getVideoChannelVideos, getVideoWithToken } from '../../utils' | ||
6 | import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils/index' | ||
7 | import { unfollow } from '../../utils/server/follows' | ||
8 | import { userLogin } from '../../utils/users/login' | ||
9 | import { createUser } from '../../utils/users/users' | ||
10 | |||
11 | const expect = chai.expect | ||
12 | |||
13 | describe('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 @@ | |||
1 | import './config' | 1 | import './config' |
2 | import './email' | 2 | import './email' |
3 | import './follow-constraints' | ||
3 | import './follows' | 4 | import './follows' |
4 | import './handle-down' | 5 | import './handle-down' |
5 | import './jobs' | 6 | import './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 | ``` | ||
40 | externalDocs: | ||
41 | url: https://docs.joinpeertube.org/api.html | ||
25 | tags: | 42 | tags: |
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. | ||
94 | x-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 | ||
83 | paths: | 124 | paths: |
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' |
1098 | servers: | 1285 | servers: |
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) | ||
1101 | components: | 1292 | components: |
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: |