Merge branch 'develop' of https://github.com/Chocobozzz/PeerTube into move-utils-to-shared
This commit is contained in:
commit
b9f234371b
|
@ -124,6 +124,10 @@ function sortBy (obj: any[], key1: string, key2?: string) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollToTop () {
|
||||||
|
window.scroll(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
sortBy,
|
sortBy,
|
||||||
durationToString,
|
durationToString,
|
||||||
|
@ -135,5 +139,6 @@ export {
|
||||||
immutableAssign,
|
immutableAssign,
|
||||||
objectToFormData,
|
objectToFormData,
|
||||||
lineFeedToHtml,
|
lineFeedToHtml,
|
||||||
removeElementFromArray
|
removeElementFromArray,
|
||||||
|
scrollToTop
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
Create an account
|
Create an account
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="info" class="alert alert-info">{{ info }}</div>
|
||||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-left flex-wrap">
|
<div class="d-flex justify-content-left flex-wrap">
|
||||||
|
@ -59,7 +60,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="submit" i18n-value value="Signup" [disabled]="!form.valid">
|
<input type="submit" i18n-value value="Signup" [disabled]="!form.valid || signupDone">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'
|
||||||
import { NotificationsService } from 'angular2-notifications'
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
import { UserCreate } from '../../../../shared'
|
import { UserCreate } from '../../../../shared'
|
||||||
import { FormReactive, UserService, UserValidatorsService } from '../shared'
|
import { FormReactive, UserService, UserValidatorsService } from '../shared'
|
||||||
import { RedirectService, ServerService } from '@app/core'
|
import { AuthService, RedirectService, ServerService } from '@app/core'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||||
|
|
||||||
|
@ -12,10 +12,13 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val
|
||||||
styleUrls: [ './signup.component.scss' ]
|
styleUrls: [ './signup.component.scss' ]
|
||||||
})
|
})
|
||||||
export class SignupComponent extends FormReactive implements OnInit {
|
export class SignupComponent extends FormReactive implements OnInit {
|
||||||
|
info: string = null
|
||||||
error: string = null
|
error: string = null
|
||||||
|
signupDone = false
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected formValidatorService: FormValidatorService,
|
protected formValidatorService: FormValidatorService,
|
||||||
|
private authService: AuthService,
|
||||||
private userValidatorsService: UserValidatorsService,
|
private userValidatorsService: UserValidatorsService,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
|
@ -50,20 +53,29 @@ export class SignupComponent extends FormReactive implements OnInit {
|
||||||
|
|
||||||
this.userService.signup(userCreate).subscribe(
|
this.userService.signup(userCreate).subscribe(
|
||||||
() => {
|
() => {
|
||||||
|
this.signupDone = true
|
||||||
|
|
||||||
if (this.requiresEmailVerification) {
|
if (this.requiresEmailVerification) {
|
||||||
this.notificationsService.alert(
|
this.info = this.i18n('Welcome! Now please check your emails to verify your account and complete signup.')
|
||||||
this.i18n('Welcome'),
|
return
|
||||||
this.i18n('Please check your email to verify your account and complete signup.')
|
}
|
||||||
)
|
|
||||||
} else {
|
// Auto login
|
||||||
|
this.authService.login(userCreate.username, userCreate.password)
|
||||||
|
.subscribe(
|
||||||
|
() => {
|
||||||
this.notificationsService.success(
|
this.notificationsService.success(
|
||||||
this.i18n('Success'),
|
this.i18n('Success'),
|
||||||
this.i18n('Registration for {{username}} complete.', { username: userCreate.username })
|
this.i18n('You are now logged in as {{username}}!', { username: userCreate.username })
|
||||||
)
|
)
|
||||||
}
|
|
||||||
this.redirectService.redirectToHomepage()
|
this.redirectService.redirectToHomepage()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
err => this.error = err.message
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
err => this.error = err.message
|
err => this.error = err.message
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
|
||||||
hide () {
|
hide () {
|
||||||
this.closingModal = true
|
this.closingModal = true
|
||||||
this.openedModal.close()
|
this.openedModal.close()
|
||||||
|
this.form.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
isReplacingExistingCaption () {
|
isReplacingExistingCaption () {
|
||||||
|
|
|
@ -45,7 +45,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="hasImportedVideo" class="alert alert-info" i18n>
|
<div *ngIf="error" class="alert alert-danger">
|
||||||
|
<div i18n>Sorry, but something went wrong</div>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="hasImportedVideo && !error" class="alert alert-info" i18n>
|
||||||
Congratulations, the video will be imported with BitTorrent! You can already add information about this video.
|
Congratulations, the video will be imported with BitTorrent! You can already add information about this video.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,14 @@ $width-size: 190px;
|
||||||
@include peertube-select-container($width-size);
|
@include peertube-select-container($width-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert.alert-danger {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
font-weight: $font-semibold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.import-video-torrent {
|
.import-video-torrent {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
|
||||||
import { FormValidatorService } from '@app/shared'
|
import { FormValidatorService } from '@app/shared'
|
||||||
import { VideoCaptionService } from '@app/shared/video-caption'
|
import { VideoCaptionService } from '@app/shared/video-caption'
|
||||||
import { VideoImportService } from '@app/shared/video-import'
|
import { VideoImportService } from '@app/shared/video-import'
|
||||||
|
import { scrollToTop } from '@app/shared/misc/utils'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-import-torrent',
|
selector: 'my-video-import-torrent',
|
||||||
|
@ -23,9 +24,9 @@ import { VideoImportService } from '@app/shared/video-import'
|
||||||
})
|
})
|
||||||
export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate {
|
export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate {
|
||||||
@Output() firstStepDone = new EventEmitter<string>()
|
@Output() firstStepDone = new EventEmitter<string>()
|
||||||
|
@Output() firstStepError = new EventEmitter<void>()
|
||||||
@ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement>
|
@ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement>
|
||||||
|
|
||||||
videoFileName: string
|
|
||||||
magnetUri = ''
|
magnetUri = ''
|
||||||
|
|
||||||
isImportingVideo = false
|
isImportingVideo = false
|
||||||
|
@ -33,6 +34,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
|
||||||
isUpdatingVideo = false
|
isUpdatingVideo = false
|
||||||
|
|
||||||
video: VideoEdit
|
video: VideoEdit
|
||||||
|
error: string
|
||||||
|
|
||||||
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
|
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
|
||||||
|
|
||||||
|
@ -104,6 +106,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
|
||||||
err => {
|
err => {
|
||||||
this.loadingBar.complete()
|
this.loadingBar.complete()
|
||||||
this.isImportingVideo = false
|
this.isImportingVideo = false
|
||||||
|
this.firstStepError.emit()
|
||||||
this.notificationsService.error(this.i18n('Error'), err.message)
|
this.notificationsService.error(this.i18n('Error'), err.message)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -129,8 +132,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
|
||||||
},
|
},
|
||||||
|
|
||||||
err => {
|
err => {
|
||||||
this.isUpdatingVideo = false
|
this.error = err.message
|
||||||
this.notificationsService.error(this.i18n('Error'), err.message)
|
scrollToTop()
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -37,7 +37,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="hasImportedVideo" class="alert alert-info" i18n>
|
|
||||||
|
<div *ngIf="error" class="alert alert-danger">
|
||||||
|
<div i18n>Sorry, but something went wrong</div>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!error && hasImportedVideo" class="alert alert-info" i18n>
|
||||||
Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video.
|
Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,14 @@ $width-size: 190px;
|
||||||
@include peertube-select-container($width-size);
|
@include peertube-select-container($width-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert.alert-danger {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
font-weight: $font-semibold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.import-video-url {
|
.import-video-url {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
|
||||||
import { FormValidatorService } from '@app/shared'
|
import { FormValidatorService } from '@app/shared'
|
||||||
import { VideoCaptionService } from '@app/shared/video-caption'
|
import { VideoCaptionService } from '@app/shared/video-caption'
|
||||||
import { VideoImportService } from '@app/shared/video-import'
|
import { VideoImportService } from '@app/shared/video-import'
|
||||||
|
import { scrollToTop } from '@app/shared/misc/utils'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-import-url',
|
selector: 'my-video-import-url',
|
||||||
|
@ -23,15 +24,16 @@ import { VideoImportService } from '@app/shared/video-import'
|
||||||
})
|
})
|
||||||
export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate {
|
export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate {
|
||||||
@Output() firstStepDone = new EventEmitter<string>()
|
@Output() firstStepDone = new EventEmitter<string>()
|
||||||
|
@Output() firstStepError = new EventEmitter<void>()
|
||||||
|
|
||||||
targetUrl = ''
|
targetUrl = ''
|
||||||
videoFileName: string
|
|
||||||
|
|
||||||
isImportingVideo = false
|
isImportingVideo = false
|
||||||
hasImportedVideo = false
|
hasImportedVideo = false
|
||||||
isUpdatingVideo = false
|
isUpdatingVideo = false
|
||||||
|
|
||||||
video: VideoEdit
|
video: VideoEdit
|
||||||
|
error: string
|
||||||
|
|
||||||
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
|
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
|
||||||
|
|
||||||
|
@ -96,6 +98,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
|
||||||
err => {
|
err => {
|
||||||
this.loadingBar.complete()
|
this.loadingBar.complete()
|
||||||
this.isImportingVideo = false
|
this.isImportingVideo = false
|
||||||
|
this.firstStepError.emit()
|
||||||
this.notificationsService.error(this.i18n('Error'), err.message)
|
this.notificationsService.error(this.i18n('Error'), err.message)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -121,8 +124,8 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
|
||||||
},
|
},
|
||||||
|
|
||||||
err => {
|
err => {
|
||||||
this.isUpdatingVideo = false
|
this.error = err.message
|
||||||
this.notificationsService.error(this.i18n('Error'), err.message)
|
scrollToTop()
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,6 +21,7 @@ export abstract class VideoSend extends FormReactive implements OnInit {
|
||||||
firstStepChannelId = 0
|
firstStepChannelId = 0
|
||||||
|
|
||||||
abstract firstStepDone: EventEmitter<string>
|
abstract firstStepDone: EventEmitter<string>
|
||||||
|
abstract firstStepError: EventEmitter<void>
|
||||||
protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy
|
protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy
|
||||||
|
|
||||||
protected loadingBar: LoadingBarService
|
protected loadingBar: LoadingBarService
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="isUploadingVideo" class="upload-progress-cancel">
|
<div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel">
|
||||||
<p-progressBar
|
<p-progressBar
|
||||||
[value]="videoUploadPercents"
|
[value]="videoUploadPercents"
|
||||||
[ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }"
|
[ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }"
|
||||||
|
@ -37,6 +37,11 @@
|
||||||
<input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" />
|
<input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="error" class="alert alert-danger">
|
||||||
|
<div i18n>Sorry, but something went wrong</div>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Hidden because we want to load the component -->
|
<!-- Hidden because we want to load the component -->
|
||||||
<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
|
<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
|
||||||
<my-video-edit
|
<my-video-edit
|
||||||
|
|
|
@ -5,6 +5,14 @@
|
||||||
@include peertube-select-container(190px);
|
@include peertube-select-container(190px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert.alert-danger {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
font-weight: $font-semibold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.upload-video {
|
.upload-video {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { VideoSend } from '@app/videos/+video-edit/video-add-components/video-se
|
||||||
import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
|
import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
|
||||||
import { FormValidatorService, UserService } from '@app/shared'
|
import { FormValidatorService, UserService } from '@app/shared'
|
||||||
import { VideoCaptionService } from '@app/shared/video-caption'
|
import { VideoCaptionService } from '@app/shared/video-caption'
|
||||||
|
import { scrollToTop } from '@app/shared/misc/utils'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-upload',
|
selector: 'my-video-upload',
|
||||||
|
@ -25,6 +26,7 @@ import { VideoCaptionService } from '@app/shared/video-caption'
|
||||||
})
|
})
|
||||||
export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate {
|
export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||||
@Output() firstStepDone = new EventEmitter<string>()
|
@Output() firstStepDone = new EventEmitter<string>()
|
||||||
|
@Output() firstStepError = new EventEmitter<void>()
|
||||||
@ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
|
@ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
|
||||||
|
|
||||||
// So that it can be accessed in the template
|
// So that it can be accessed in the template
|
||||||
|
@ -43,6 +45,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
||||||
uuid: ''
|
uuid: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
error: string
|
||||||
|
|
||||||
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
|
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
@ -201,6 +205,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
||||||
this.isUploadingVideo = false
|
this.isUploadingVideo = false
|
||||||
this.videoUploadPercents = 0
|
this.videoUploadPercents = 0
|
||||||
this.videoUploadObservable = null
|
this.videoUploadObservable = null
|
||||||
|
this.firstStepError.emit()
|
||||||
this.notificationsService.error(this.i18n('Error'), err.message)
|
this.notificationsService.error(this.i18n('Error'), err.message)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -235,8 +240,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
||||||
},
|
},
|
||||||
|
|
||||||
err => {
|
err => {
|
||||||
this.isUpdatingVideo = false
|
this.error = err.message
|
||||||
this.notificationsService.error(this.i18n('Error'), err.message)
|
scrollToTop()
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,24 +6,24 @@
|
||||||
|
|
||||||
<ngb-tabset class="video-add-tabset root-tabset bootstrap" [ngClass]="{ 'hide-nav': secondStepType !== undefined }">
|
<ngb-tabset class="video-add-tabset root-tabset bootstrap" [ngClass]="{ 'hide-nav': secondStepType !== undefined }">
|
||||||
|
|
||||||
<ngb-tab i18n-title title="">
|
<ngb-tab>
|
||||||
<ng-template ngbTabTitle><span i18n>Upload a file</span></ng-template>
|
<ng-template ngbTabTitle><span i18n>Upload a file</span></ng-template>
|
||||||
<ng-template ngbTabContent>
|
<ng-template ngbTabContent>
|
||||||
<my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)"></my-video-upload>
|
<my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)" (firstStepError)="onError()"></my-video-upload>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ngb-tab>
|
</ngb-tab>
|
||||||
|
|
||||||
<ngb-tab *ngIf="isVideoImportHttpEnabled()">
|
<ngb-tab *ngIf="isVideoImportHttpEnabled()">
|
||||||
<ng-template ngbTabTitle><span i18n>Import with URL</span></ng-template>
|
<ng-template ngbTabTitle><span i18n>Import with URL</span></ng-template>
|
||||||
<ng-template ngbTabContent>
|
<ng-template ngbTabContent>
|
||||||
<my-video-import-url #videoImportUrl (firstStepDone)="onFirstStepDone('import-url', $event)"></my-video-import-url>
|
<my-video-import-url #videoImportUrl (firstStepDone)="onFirstStepDone('import-url', $event)" (firstStepError)="onError()"></my-video-import-url>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ngb-tab>
|
</ngb-tab>
|
||||||
|
|
||||||
<ngb-tab *ngIf="isVideoImportTorrentEnabled()">
|
<ngb-tab *ngIf="isVideoImportTorrentEnabled()">
|
||||||
<ng-template ngbTabTitle><span i18n>Import with torrent</span></ng-template>
|
<ng-template ngbTabTitle><span i18n>Import with torrent</span></ng-template>
|
||||||
<ng-template ngbTabContent>
|
<ng-template ngbTabContent>
|
||||||
<my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)"></my-video-import-torrent>
|
<my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)" (firstStepError)="onError()"></my-video-import-torrent>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ngb-tab>
|
</ngb-tab>
|
||||||
</ngb-tabset>
|
</ngb-tabset>
|
||||||
|
|
|
@ -27,6 +27,11 @@ export class VideoAddComponent implements CanComponentDeactivate {
|
||||||
this.videoName = videoName
|
this.videoName = videoName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onError () {
|
||||||
|
this.videoName = undefined
|
||||||
|
this.secondStepType = undefined
|
||||||
|
}
|
||||||
|
|
||||||
canDeactivate () {
|
canDeactivate () {
|
||||||
if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate()
|
if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate()
|
||||||
if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate()
|
if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate()
|
||||||
|
|
|
@ -114,7 +114,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
// If 401, the video is private or blacklisted so redirect to 404
|
// If 401, the video is private or blacklisted so redirect to 404
|
||||||
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 404 ]))
|
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ]))
|
||||||
)
|
)
|
||||||
.subscribe(([ video, captionsResult ]) => {
|
.subscribe(([ video, captionsResult ]) => {
|
||||||
const startTime = this.route.snapshot.queryParams.start
|
const startTime = this.route.snapshot.queryParams.start
|
||||||
|
|
Before Width: | Height: | Size: 738 B After Width: | Height: | Size: 738 B |
|
@ -111,6 +111,8 @@ class PeerTubePlugin extends Plugin {
|
||||||
const muted = getStoredMute()
|
const muted = getStoredMute()
|
||||||
if (muted !== undefined) this.player.muted(muted)
|
if (muted !== undefined) this.player.muted(muted)
|
||||||
|
|
||||||
|
this.player.duration(options.videoDuration)
|
||||||
|
|
||||||
this.initializePlayer()
|
this.initializePlayer()
|
||||||
this.runTorrentInfoScheduler()
|
this.runTorrentInfoScheduler()
|
||||||
this.runViewAdd()
|
this.runViewAdd()
|
||||||
|
@ -302,6 +304,9 @@ class PeerTubePlugin extends Plugin {
|
||||||
|
|
||||||
this.flushVideoFile(previousVideoFile)
|
this.flushVideoFile(previousVideoFile)
|
||||||
|
|
||||||
|
// Update progress bar (just for the UI), do not wait rendering
|
||||||
|
if (options.seek) this.player.currentTime(options.seek)
|
||||||
|
|
||||||
const renderVideoOptions = { autoplay: false, controls: true }
|
const renderVideoOptions = { autoplay: false, controls: true }
|
||||||
renderVideo(torrent.files[ 0 ], this.playerElement, renderVideoOptions, (err, renderer) => {
|
renderVideo(torrent.files[ 0 ], this.playerElement, renderVideoOptions, (err, renderer) => {
|
||||||
this.renderer = renderer
|
this.renderer = renderer
|
||||||
|
|
|
@ -171,7 +171,7 @@ $setting-transition-easing: ease-out;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
content: ' ';
|
content: ' ';
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
background-image: url('#{$assets-path}/player/images/tick.svg');
|
background-image: url('#{$assets-path}/player/images/tick-white.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,10 @@ log:
|
||||||
level: 'info' # debug/info/warning/error
|
level: 'info' # debug/info/warning/error
|
||||||
|
|
||||||
search:
|
search:
|
||||||
remote_uri: # Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance
|
# Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance
|
||||||
|
# If enabled, the associated group will be able to "escape" from the instance follows
|
||||||
|
# That means they will be able to follow channels, watch videos, list videos of non followed instances
|
||||||
|
remote_uri:
|
||||||
users: true
|
users: true
|
||||||
anonymous: false
|
anonymous: false
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,10 @@ log:
|
||||||
level: 'info' # debug/info/warning/error
|
level: 'info' # debug/info/warning/error
|
||||||
|
|
||||||
search:
|
search:
|
||||||
remote_uri: # Add ability to search remote videos/actors by URI, that may not be federated with your instance
|
# Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance
|
||||||
|
# If enabled, the associated group will be able to "escape" from the instance follows
|
||||||
|
# That means they will be able to follow channels, watch videos, list videos of non followed instances
|
||||||
|
remote_uri:
|
||||||
users: true
|
users: true
|
||||||
anonymous: false
|
anonymous: false
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ import {
|
||||||
import { VideoCaptionModel } from '../../models/video/video-caption'
|
import { VideoCaptionModel } from '../../models/video/video-caption'
|
||||||
import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy'
|
import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy'
|
||||||
import { getServerActor } from '../../helpers/utils'
|
import { getServerActor } from '../../helpers/utils'
|
||||||
|
import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
|
||||||
|
|
||||||
const activityPubClientRouter = express.Router()
|
const activityPubClientRouter = express.Router()
|
||||||
|
|
||||||
|
@ -164,6 +165,8 @@ function getAccountVideoRate (rateType: VideoRateType) {
|
||||||
async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
|
async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
const video: VideoModel = res.locals.video
|
const video: VideoModel = res.locals.video
|
||||||
|
|
||||||
|
if (video.isOwned() === false) return res.redirect(video.url)
|
||||||
|
|
||||||
// We need captions to render AP object
|
// We need captions to render AP object
|
||||||
video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id)
|
video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id)
|
||||||
|
|
||||||
|
@ -180,6 +183,9 @@ async function videoController (req: express.Request, res: express.Response, nex
|
||||||
|
|
||||||
async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) {
|
async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
const share = res.locals.videoShare as VideoShareModel
|
const share = res.locals.videoShare as VideoShareModel
|
||||||
|
|
||||||
|
if (share.Actor.isOwned() === false) return res.redirect(share.url)
|
||||||
|
|
||||||
const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined)
|
const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined)
|
||||||
|
|
||||||
return activityPubResponse(activityPubContextify(activity), res)
|
return activityPubResponse(activityPubContextify(activity), res)
|
||||||
|
@ -252,6 +258,8 @@ async function videoChannelFollowingController (req: express.Request, res: expre
|
||||||
async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) {
|
async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
const videoComment: VideoCommentModel = res.locals.videoComment
|
const videoComment: VideoCommentModel = res.locals.videoComment
|
||||||
|
|
||||||
|
if (videoComment.isOwned() === false) return res.redirect(videoComment.url)
|
||||||
|
|
||||||
const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
|
const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
|
||||||
const isPublic = true // Comments are always public
|
const isPublic = true // Comments are always public
|
||||||
const audience = getAudience(videoComment.Account.Actor, isPublic)
|
const audience = getAudience(videoComment.Account.Actor, isPublic)
|
||||||
|
@ -267,7 +275,9 @@ async function videoCommentController (req: express.Request, res: express.Respon
|
||||||
}
|
}
|
||||||
|
|
||||||
async function videoRedundancyController (req: express.Request, res: express.Response) {
|
async function videoRedundancyController (req: express.Request, res: express.Response) {
|
||||||
const videoRedundancy = res.locals.videoRedundancy
|
const videoRedundancy: VideoRedundancyModel = res.locals.videoRedundancy
|
||||||
|
if (videoRedundancy.isOwned() === false) return res.redirect(videoRedundancy.url)
|
||||||
|
|
||||||
const serverActor = await getServerActor()
|
const serverActor = await getServerActor()
|
||||||
|
|
||||||
const audience = getAudience(serverActor)
|
const audience = getAudience(serverActor)
|
||||||
|
@ -288,7 +298,7 @@ async function actorFollowing (req: express.Request, actor: ActorModel) {
|
||||||
return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count)
|
return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, handler, req.query.page)
|
return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.path, handler, req.query.page)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function actorFollowers (req: express.Request, actor: ActorModel) {
|
async function actorFollowers (req: express.Request, actor: ActorModel) {
|
||||||
|
@ -296,7 +306,7 @@ async function actorFollowers (req: express.Request, actor: ActorModel) {
|
||||||
return ActorFollowModel.listAcceptedFollowerUrlsForApi([ actor.id ], undefined, start, count)
|
return ActorFollowModel.listAcceptedFollowerUrlsForApi([ actor.id ], undefined, start, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, handler, req.query.page)
|
return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.path, handler, req.query.page)
|
||||||
}
|
}
|
||||||
|
|
||||||
function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) {
|
function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) {
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
asyncRetryTransactionMiddleware,
|
asyncRetryTransactionMiddleware,
|
||||||
authenticate,
|
authenticate,
|
||||||
|
checkVideoFollowConstraints,
|
||||||
commonVideosFiltersValidator,
|
commonVideosFiltersValidator,
|
||||||
optionalAuthenticate,
|
optionalAuthenticate,
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
|
@ -123,6 +124,7 @@ videosRouter.get('/:id/description',
|
||||||
videosRouter.get('/:id',
|
videosRouter.get('/:id',
|
||||||
optionalAuthenticate,
|
optionalAuthenticate,
|
||||||
asyncMiddleware(videosGetValidator),
|
asyncMiddleware(videosGetValidator),
|
||||||
|
asyncMiddleware(checkVideoFollowConstraints),
|
||||||
getVideo
|
getVideo
|
||||||
)
|
)
|
||||||
videosRouter.post('/:id/views',
|
videosRouter.post('/:id/views',
|
||||||
|
|
|
@ -57,16 +57,16 @@ function activityPubContextify <T> (data: T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>>
|
type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>>
|
||||||
async function activityPubCollectionPagination (url: string, handler: ActivityPubCollectionPaginationHandler, page?: any) {
|
async function activityPubCollectionPagination (baseUrl: string, handler: ActivityPubCollectionPaginationHandler, page?: any) {
|
||||||
if (!page || !validator.isInt(page)) {
|
if (!page || !validator.isInt(page)) {
|
||||||
// We just display the first page URL, we only need the total items
|
// We just display the first page URL, we only need the total items
|
||||||
const result = await handler(0, 1)
|
const result = await handler(0, 1)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: url,
|
id: baseUrl,
|
||||||
type: 'OrderedCollection',
|
type: 'OrderedCollection',
|
||||||
totalItems: result.total,
|
totalItems: result.total,
|
||||||
first: url + '?page=1'
|
first: baseUrl + '?page=1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,19 +81,19 @@ async function activityPubCollectionPagination (url: string, handler: ActivityPu
|
||||||
|
|
||||||
// There are more results
|
// There are more results
|
||||||
if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) {
|
if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) {
|
||||||
next = url + '?page=' + (page + 1)
|
next = baseUrl + '?page=' + (page + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page > 1) {
|
if (page > 1) {
|
||||||
prev = url + '?page=' + (page - 1)
|
prev = baseUrl + '?page=' + (page - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: url + '?page=' + page,
|
id: baseUrl + '?page=' + page,
|
||||||
type: 'OrderedCollectionPage',
|
type: 'OrderedCollectionPage',
|
||||||
prev,
|
prev,
|
||||||
next,
|
next,
|
||||||
partOf: url,
|
partOf: baseUrl,
|
||||||
orderedItems: result.data,
|
orderedItems: result.data,
|
||||||
totalItems: result.total
|
totalItems: result.total
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import * as Bluebird from 'bluebird'
|
||||||
import { createWriteStream } from 'fs-extra'
|
import { createWriteStream } from 'fs-extra'
|
||||||
import * as request from 'request'
|
import * as request from 'request'
|
||||||
import { ACTIVITY_PUB } from '../initializers'
|
import { ACTIVITY_PUB } from '../initializers'
|
||||||
|
import { processImage } from './image-utils'
|
||||||
|
|
||||||
function doRequest <T> (
|
function doRequest <T> (
|
||||||
requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
|
requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
|
||||||
|
@ -27,9 +28,18 @@ function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.U
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function downloadImage (url: string, destPath: string, size: { width: number, height: number }) {
|
||||||
|
const tmpPath = destPath + '.tmp'
|
||||||
|
|
||||||
|
await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath)
|
||||||
|
|
||||||
|
await processImage({ path: tmpPath }, destPath, size)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
doRequest,
|
doRequest,
|
||||||
doRequestAndSaveToFile
|
doRequestAndSaveToFile,
|
||||||
|
downloadImage
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,9 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp
|
||||||
import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
|
import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
|
import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
|
||||||
import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
|
import { doRequest, doRequestAndSaveToFile, downloadImage } from '../../helpers/requests'
|
||||||
import { getUrlFromWebfinger } from '../../helpers/webfinger'
|
import { getUrlFromWebfinger } from '../../helpers/webfinger'
|
||||||
import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers'
|
import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, PREVIEWS_SIZE, sequelizeTypescript } from '../../initializers'
|
||||||
import { AccountModel } from '../../models/account/account'
|
import { AccountModel } from '../../models/account/account'
|
||||||
import { ActorModel } from '../../models/activitypub/actor'
|
import { ActorModel } from '../../models/activitypub/actor'
|
||||||
import { AvatarModel } from '../../models/avatar/avatar'
|
import { AvatarModel } from '../../models/avatar/avatar'
|
||||||
|
@ -180,10 +180,7 @@ async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
|
||||||
const avatarName = uuidv4() + extension
|
const avatarName = uuidv4() + extension
|
||||||
const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
|
const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
|
||||||
|
|
||||||
await doRequestAndSaveToFile({
|
await downloadImage(actorJSON.icon.url, destPath, AVATARS_SIZE)
|
||||||
method: 'GET',
|
|
||||||
uri: actorJSON.icon.url
|
|
||||||
}, destPath)
|
|
||||||
|
|
||||||
return avatarName
|
return avatarName
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validat
|
||||||
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
|
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
|
||||||
import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
|
import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
|
import { doRequest, downloadImage } from '../../helpers/requests'
|
||||||
import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers'
|
import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_MIMETYPE_EXT } from '../../initializers'
|
||||||
import { ActorModel } from '../../models/activitypub/actor'
|
import { ActorModel } from '../../models/activitypub/actor'
|
||||||
import { TagModel } from '../../models/video/tag'
|
import { TagModel } from '../../models/video/tag'
|
||||||
import { VideoModel } from '../../models/video/video'
|
import { VideoModel } from '../../models/video/video'
|
||||||
|
@ -97,11 +97,7 @@ function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject)
|
||||||
const thumbnailName = video.getThumbnailName()
|
const thumbnailName = video.getThumbnailName()
|
||||||
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
|
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
|
||||||
|
|
||||||
const options = {
|
return downloadImage(icon.url, thumbnailPath, THUMBNAILS_SIZE)
|
||||||
method: 'GET',
|
|
||||||
uri: icon.url
|
|
||||||
}
|
|
||||||
return doRequestAndSaveToFile(options, thumbnailPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
|
function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
|
||||||
|
|
|
@ -6,8 +6,8 @@ import { VideoImportState } from '../../../../shared/models/videos'
|
||||||
import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
|
import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
|
||||||
import { extname, join } from 'path'
|
import { extname, join } from 'path'
|
||||||
import { VideoFileModel } from '../../../models/video/video-file'
|
import { VideoFileModel } from '../../../models/video/video-file'
|
||||||
import { CONFIG, sequelizeTypescript, VIDEO_IMPORT_TIMEOUT } from '../../../initializers'
|
import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers'
|
||||||
import { doRequestAndSaveToFile } from '../../../helpers/requests'
|
import { doRequestAndSaveToFile, downloadImage } from '../../../helpers/requests'
|
||||||
import { VideoState } from '../../../../shared'
|
import { VideoState } from '../../../../shared'
|
||||||
import { JobQueue } from '../index'
|
import { JobQueue } from '../index'
|
||||||
import { federateVideoIfNeeded } from '../../activitypub'
|
import { federateVideoIfNeeded } from '../../activitypub'
|
||||||
|
@ -133,7 +133,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
|
||||||
videoId: videoImport.videoId
|
videoId: videoImport.videoId
|
||||||
}
|
}
|
||||||
videoFile = new VideoFileModel(videoFileData)
|
videoFile = new VideoFileModel(videoFileData)
|
||||||
// Import if the import fails, to clean files
|
// To clean files if the import fails
|
||||||
videoImport.Video.VideoFiles = [ videoFile ]
|
videoImport.Video.VideoFiles = [ videoFile ]
|
||||||
|
|
||||||
// Move file
|
// Move file
|
||||||
|
@ -145,7 +145,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
|
||||||
if (options.downloadThumbnail) {
|
if (options.downloadThumbnail) {
|
||||||
if (options.thumbnailUrl) {
|
if (options.thumbnailUrl) {
|
||||||
const destThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName())
|
const destThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName())
|
||||||
await doRequestAndSaveToFile({ method: 'GET', uri: options.thumbnailUrl }, destThumbnailPath)
|
await downloadImage(options.thumbnailUrl, destThumbnailPath, THUMBNAILS_SIZE)
|
||||||
} else {
|
} else {
|
||||||
await videoImport.Video.createThumbnail(videoFile)
|
await videoImport.Video.createThumbnail(videoFile)
|
||||||
}
|
}
|
||||||
|
@ -157,7 +157,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
|
||||||
if (options.downloadPreview) {
|
if (options.downloadPreview) {
|
||||||
if (options.thumbnailUrl) {
|
if (options.thumbnailUrl) {
|
||||||
const destPreviewPath = join(CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName())
|
const destPreviewPath = join(CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName())
|
||||||
await doRequestAndSaveToFile({ method: 'GET', uri: options.thumbnailUrl }, destPreviewPath)
|
await downloadImage(options.thumbnailUrl, destPreviewPath, PREVIEWS_SIZE)
|
||||||
} else {
|
} else {
|
||||||
await videoImport.Video.createPreview(videoFile)
|
await videoImport.Video.createPreview(videoFile)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ function cacheRoute (lifetimeArg: string | number) {
|
||||||
logger.debug('No cached results for route %s.', req.originalUrl)
|
logger.debug('No cached results for route %s.', req.originalUrl)
|
||||||
|
|
||||||
const sendSave = res.send.bind(res)
|
const sendSave = res.send.bind(res)
|
||||||
|
const redirectSave = res.redirect.bind(res)
|
||||||
|
|
||||||
res.send = (body) => {
|
res.send = (body) => {
|
||||||
if (res.statusCode >= 200 && res.statusCode < 400) {
|
if (res.statusCode >= 200 && res.statusCode < 400) {
|
||||||
|
@ -38,6 +39,12 @@ function cacheRoute (lifetimeArg: string | number) {
|
||||||
return sendSave(body)
|
return sendSave(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.redirect = url => {
|
||||||
|
done()
|
||||||
|
|
||||||
|
return redirectSave(url)
|
||||||
|
}
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,9 +28,24 @@ function authenticate (req: express.Request, res: express.Response, next: expres
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function authenticatePromiseIfNeeded (req: express.Request, res: express.Response) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
// Already authenticated? (or tried to)
|
||||||
|
if (res.locals.oauth && res.locals.oauth.token.User) return resolve()
|
||||||
|
|
||||||
|
if (res.locals.authenticated === false) return res.sendStatus(401)
|
||||||
|
|
||||||
|
authenticate(req, res, () => {
|
||||||
|
return resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) {
|
function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
if (req.header('authorization')) return authenticate(req, res, next)
|
if (req.header('authorization')) return authenticate(req, res, next)
|
||||||
|
|
||||||
|
res.locals.authenticated = false
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +68,7 @@ function token (req: express.Request, res: express.Response, next: express.NextF
|
||||||
|
|
||||||
export {
|
export {
|
||||||
authenticate,
|
authenticate,
|
||||||
|
authenticatePromiseIfNeeded,
|
||||||
optionalAuthenticate,
|
optionalAuthenticate,
|
||||||
token
|
token
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,8 @@ import {
|
||||||
} from '../../../helpers/custom-validators/videos'
|
} from '../../../helpers/custom-validators/videos'
|
||||||
import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
|
import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../../initializers'
|
import { CONFIG, CONSTRAINTS_FIELDS } from '../../../initializers'
|
||||||
import { authenticate } from '../../oauth'
|
import { authenticatePromiseIfNeeded } from '../../oauth'
|
||||||
import { areValidationErrors } from '../utils'
|
import { areValidationErrors } from '../utils'
|
||||||
import { cleanUpReqFiles } from '../../../helpers/express-utils'
|
import { cleanUpReqFiles } from '../../../helpers/express-utils'
|
||||||
import { VideoModel } from '../../../models/video/video'
|
import { VideoModel } from '../../../models/video/video'
|
||||||
|
@ -43,6 +43,7 @@ import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ow
|
||||||
import { AccountModel } from '../../../models/account/account'
|
import { AccountModel } from '../../../models/account/account'
|
||||||
import { VideoFetchType } from '../../../helpers/video'
|
import { VideoFetchType } from '../../../helpers/video'
|
||||||
import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
|
import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
|
||||||
|
import { getServerActor } from '../../../helpers/utils'
|
||||||
|
|
||||||
const videosAddValidator = getCommonVideoAttributes().concat([
|
const videosAddValidator = getCommonVideoAttributes().concat([
|
||||||
body('videofile')
|
body('videofile')
|
||||||
|
@ -127,6 +128,31 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
|
const video: VideoModel = res.locals.video
|
||||||
|
|
||||||
|
// Anybody can watch local videos
|
||||||
|
if (video.isOwned() === true) return next()
|
||||||
|
|
||||||
|
// Logged user
|
||||||
|
if (res.locals.oauth) {
|
||||||
|
// Users can search or watch remote videos
|
||||||
|
if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anybody can search or watch remote videos
|
||||||
|
if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
|
||||||
|
|
||||||
|
// Check our instance follows an actor that shared this video
|
||||||
|
const serverActor = await getServerActor()
|
||||||
|
if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
|
||||||
|
|
||||||
|
return res.status(403)
|
||||||
|
.json({
|
||||||
|
error: 'Cannot get this video regarding follow constraints.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const videosCustomGetValidator = (fetchType: VideoFetchType) => {
|
const videosCustomGetValidator = (fetchType: VideoFetchType) => {
|
||||||
return [
|
return [
|
||||||
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
|
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
|
||||||
|
@ -141,17 +167,20 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => {
|
||||||
|
|
||||||
// Video private or blacklisted
|
// Video private or blacklisted
|
||||||
if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
|
if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
|
||||||
return authenticate(req, res, () => {
|
await authenticatePromiseIfNeeded(req, res)
|
||||||
const user: UserModel = res.locals.oauth.token.User
|
|
||||||
|
const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : null
|
||||||
|
|
||||||
// Only the owner or a user that have blacklist rights can see the video
|
// Only the owner or a user that have blacklist rights can see the video
|
||||||
if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
|
if (
|
||||||
|
!user ||
|
||||||
|
(video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
|
||||||
|
) {
|
||||||
return res.status(403)
|
return res.status(403)
|
||||||
.json({ error: 'Cannot get this private or blacklisted video.' })
|
.json({ error: 'Cannot get this private or blacklisted video.' })
|
||||||
}
|
}
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video is public, anyone can access it
|
// Video is public, anyone can access it
|
||||||
|
@ -376,6 +405,7 @@ export {
|
||||||
videosAddValidator,
|
videosAddValidator,
|
||||||
videosUpdateValidator,
|
videosUpdateValidator,
|
||||||
videosGetValidator,
|
videosGetValidator,
|
||||||
|
checkVideoFollowConstraints,
|
||||||
videosCustomGetValidator,
|
videosCustomGetValidator,
|
||||||
videosRemoveValidator,
|
videosRemoveValidator,
|
||||||
|
|
||||||
|
@ -393,6 +423,8 @@ export {
|
||||||
function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
|
function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
|
||||||
if (req.body.scheduleUpdate) {
|
if (req.body.scheduleUpdate) {
|
||||||
if (!req.body.scheduleUpdate.updateAt) {
|
if (!req.body.scheduleUpdate.updateAt) {
|
||||||
|
logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
|
||||||
|
|
||||||
res.status(400)
|
res.status(400)
|
||||||
.json({ error: 'Schedule update at is mandatory.' })
|
.json({ error: 'Schedule update at is mandatory.' })
|
||||||
|
|
||||||
|
|
|
@ -509,12 +509,12 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
|
||||||
tasks.push(ActorFollowModel.sequelize.query(query, options))
|
tasks.push(ActorFollowModel.sequelize.query(query, options))
|
||||||
}
|
}
|
||||||
|
|
||||||
const [ followers, [ { total } ] ] = await Promise.all(tasks)
|
const [ followers, [ dataTotal ] ] = await Promise.all(tasks)
|
||||||
const urls: string[] = followers.map(f => f.url)
|
const urls: string[] = followers.map(f => f.url)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: urls,
|
data: urls,
|
||||||
total: parseInt(total, 10)
|
total: dataTotal ? parseInt(dataTotal.total, 10) : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -117,8 +117,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
|
||||||
|
|
||||||
@BeforeDestroy
|
@BeforeDestroy
|
||||||
static async removeFile (instance: VideoRedundancyModel) {
|
static async removeFile (instance: VideoRedundancyModel) {
|
||||||
// Not us
|
if (!instance.isOwned()) return
|
||||||
if (!instance.strategy) return
|
|
||||||
|
|
||||||
const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId)
|
const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId)
|
||||||
|
|
||||||
|
@ -404,6 +403,10 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isOwned () {
|
||||||
|
return !!this.strategy
|
||||||
|
}
|
||||||
|
|
||||||
toActivityPubObject (): CacheFileObject {
|
toActivityPubObject (): CacheFileObject {
|
||||||
return {
|
return {
|
||||||
id: this.url,
|
id: this.url,
|
||||||
|
|
|
@ -1253,6 +1253,23 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
|
||||||
|
// Instances only share videos
|
||||||
|
const query = 'SELECT 1 FROM "videoShare" ' +
|
||||||
|
'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
|
||||||
|
'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' +
|
||||||
|
'LIMIT 1'
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
type: Sequelize.QueryTypes.SELECT,
|
||||||
|
bind: { followerActorId, videoId },
|
||||||
|
raw: true
|
||||||
|
}
|
||||||
|
|
||||||
|
return VideoModel.sequelize.query(query, options)
|
||||||
|
.then(results => results.length === 1)
|
||||||
|
}
|
||||||
|
|
||||||
// threshold corresponds to how many video the field should have to be returned
|
// threshold corresponds to how many video the field should have to be returned
|
||||||
static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
|
static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
|
||||||
const serverActor = await getServerActor()
|
const serverActor = await getServerActor()
|
||||||
|
|
|
@ -14,11 +14,13 @@ import {
|
||||||
setAccessTokensToServers,
|
setAccessTokensToServers,
|
||||||
userLogin
|
userLogin
|
||||||
} from '../../../../shared/utils'
|
} from '../../../../shared/utils'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
checkBadCountPagination,
|
checkBadCountPagination,
|
||||||
checkBadSortPagination,
|
checkBadSortPagination,
|
||||||
checkBadStartPagination
|
checkBadStartPagination
|
||||||
} from '../../../../shared/utils/requests/check-api-params'
|
} from '../../../../shared/utils/requests/check-api-params'
|
||||||
|
import { waitJobs } from '../../../../shared/utils/server/jobs'
|
||||||
|
|
||||||
describe('Test user subscriptions API validators', function () {
|
describe('Test user subscriptions API validators', function () {
|
||||||
const path = '/api/v1/users/me/subscriptions'
|
const path = '/api/v1/users/me/subscriptions'
|
||||||
|
@ -145,6 +147,8 @@ describe('Test user subscriptions API validators', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should succeed with the correct parameters', async function () {
|
it('Should succeed with the correct parameters', async function () {
|
||||||
|
this.timeout(20000)
|
||||||
|
|
||||||
await makePostBodyRequest({
|
await makePostBodyRequest({
|
||||||
url: server.url,
|
url: server.url,
|
||||||
path,
|
path,
|
||||||
|
@ -152,6 +156,8 @@ describe('Test user subscriptions API validators', function () {
|
||||||
fields: { uri: 'user1_channel@localhost:9001' },
|
fields: { uri: 'user1_channel@localhost:9001' },
|
||||||
statusCodeExpected: 204
|
statusCodeExpected: 204
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await waitJobs([ server ])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,10 @@ import {
|
||||||
viewVideo,
|
viewVideo,
|
||||||
wait,
|
wait,
|
||||||
waitUntilLog,
|
waitUntilLog,
|
||||||
checkVideoFilesWereRemoved, removeVideo
|
checkVideoFilesWereRemoved, removeVideo, getVideoWithToken
|
||||||
} from '../../../../shared/utils'
|
} from '../../../../shared/utils'
|
||||||
import { waitJobs } from '../../../../shared/utils/server/jobs'
|
import { waitJobs } from '../../../../shared/utils/server/jobs'
|
||||||
|
|
||||||
import * as magnetUtil from 'magnet-uri'
|
import * as magnetUtil from 'magnet-uri'
|
||||||
import { updateRedundancy } from '../../../../shared/utils/server/redundancy'
|
import { updateRedundancy } from '../../../../shared/utils/server/redundancy'
|
||||||
import { ActorFollow } from '../../../../shared/models/actors'
|
import { ActorFollow } from '../../../../shared/models/actors'
|
||||||
|
@ -93,7 +94,8 @@ async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: str
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
{
|
{
|
||||||
const res = await getVideo(server.url, videoUUID)
|
// With token to avoid issues with video follow constraints
|
||||||
|
const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
|
||||||
|
|
||||||
const video: VideoDetails = res.body
|
const video: VideoDetails = res.body
|
||||||
for (const f of video.files) {
|
for (const f of video.files) {
|
||||||
|
|
|
@ -0,0 +1,215 @@
|
||||||
|
/* tslint:disable:no-unused-expression */
|
||||||
|
|
||||||
|
import * as chai from 'chai'
|
||||||
|
import 'mocha'
|
||||||
|
import { doubleFollow, getAccountVideos, getVideo, getVideoChannelVideos, getVideoWithToken } from '../../utils'
|
||||||
|
import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils/index'
|
||||||
|
import { unfollow } from '../../utils/server/follows'
|
||||||
|
import { userLogin } from '../../utils/users/login'
|
||||||
|
import { createUser } from '../../utils/users/users'
|
||||||
|
|
||||||
|
const expect = chai.expect
|
||||||
|
|
||||||
|
describe('Test follow constraints', function () {
|
||||||
|
let servers: ServerInfo[] = []
|
||||||
|
let video1UUID: string
|
||||||
|
let video2UUID: string
|
||||||
|
let userAccessToken: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(30000)
|
||||||
|
|
||||||
|
servers = await flushAndRunMultipleServers(2)
|
||||||
|
|
||||||
|
// Get the access tokens
|
||||||
|
await setAccessTokensToServers(servers)
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video server 1' })
|
||||||
|
video1UUID = res.body.video.uuid
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video server 2' })
|
||||||
|
video2UUID = res.body.video.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
username: 'user1',
|
||||||
|
password: 'super_password'
|
||||||
|
}
|
||||||
|
await createUser(servers[0].url, servers[0].accessToken, user.username, user.password)
|
||||||
|
userAccessToken = await userLogin(servers[0], user)
|
||||||
|
|
||||||
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('With a followed instance', function () {
|
||||||
|
|
||||||
|
describe('With an unlogged user', function () {
|
||||||
|
|
||||||
|
it('Should get the local video', async function () {
|
||||||
|
await getVideo(servers[0].url, video1UUID, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should get the remote video', async function () {
|
||||||
|
await getVideo(servers[0].url, video2UUID, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list local account videos', async function () {
|
||||||
|
const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9001', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
expect(res.body.data).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list remote account videos', async function () {
|
||||||
|
const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9002', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
expect(res.body.data).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list local channel videos', async function () {
|
||||||
|
const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9001', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
expect(res.body.data).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list remote channel videos', async function () {
|
||||||
|
const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9002', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
expect(res.body.data).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('With a logged user', function () {
|
||||||
|
it('Should get the local video', async function () {
|
||||||
|
await getVideoWithToken(servers[0].url, userAccessToken, video1UUID, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should get the remote video', async function () {
|
||||||
|
await getVideoWithToken(servers[0].url, userAccessToken, video2UUID, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list local account videos', async function () {
|
||||||
|
const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9001', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
expect(res.body.data).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list remote account videos', async function () {
|
||||||
|
const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9002', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
expect(res.body.data).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list local channel videos', async function () {
|
||||||
|
const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9001', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
expect(res.body.data).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list remote channel videos', async function () {
|
||||||
|
const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9002', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
expect(res.body.data).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('With a non followed instance', function () {
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(30000)
|
||||||
|
|
||||||
|
await unfollow(servers[0].url, servers[0].accessToken, servers[1])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('With an unlogged user', function () {
|
||||||
|
|
||||||
|
it('Should get the local video', async function () {
|
||||||
|
await getVideo(servers[0].url, video1UUID, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not get the remote video', async function () {
|
||||||
|
await getVideo(servers[0].url, video2UUID, 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list local account videos', async function () {
|
||||||
|
const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9001', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
expect(res.body.data).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not list remote account videos', async function () {
|
||||||
|
const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9002', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(0)
|
||||||
|
expect(res.body.data).to.have.lengthOf(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list local channel videos', async function () {
|
||||||
|
const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9001', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
expect(res.body.data).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not list remote channel videos', async function () {
|
||||||
|
const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9002', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(0)
|
||||||
|
expect(res.body.data).to.have.lengthOf(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('With a logged user', function () {
|
||||||
|
it('Should get the local video', async function () {
|
||||||
|
await getVideoWithToken(servers[0].url, userAccessToken, video1UUID, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should get the remote video', async function () {
|
||||||
|
await getVideoWithToken(servers[0].url, userAccessToken, video2UUID, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list local account videos', async function () {
|
||||||
|
const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9001', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
expect(res.body.data).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list remote account videos', async function () {
|
||||||
|
const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9002', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
expect(res.body.data).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list local channel videos', async function () {
|
||||||
|
const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9001', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
expect(res.body.data).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list remote channel videos', async function () {
|
||||||
|
const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9002', 0, 5)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
expect(res.body.data).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
killallServers(servers)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,5 +1,6 @@
|
||||||
import './config'
|
import './config'
|
||||||
import './email'
|
import './email'
|
||||||
|
import './follow-constraints'
|
||||||
import './follows'
|
import './follows'
|
||||||
import './handle-down'
|
import './handle-down'
|
||||||
import './jobs'
|
import './jobs'
|
||||||
|
|
|
@ -10,27 +10,41 @@ info:
|
||||||
url: 'https://github.com/Chocobozzz/PeerTube/blob/master/LICENSE'
|
url: 'https://github.com/Chocobozzz/PeerTube/blob/master/LICENSE'
|
||||||
x-logo:
|
x-logo:
|
||||||
url: 'https://joinpeertube.org/img/brand.png'
|
url: 'https://joinpeertube.org/img/brand.png'
|
||||||
|
altText: PeerTube Project Homepage
|
||||||
description: |
|
description: |
|
||||||
# Introduction
|
# Introduction
|
||||||
The PeerTube API is built on HTTP(S). Our API is RESTful. It has predictable
|
The PeerTube API is built on HTTP(S). Our API is RESTful. It has predictable
|
||||||
resource URLs. It returns HTTP response codes to indicate errors. It also
|
resource URLs. It returns HTTP response codes to indicate errors. It also
|
||||||
accepts and returns JSON in the HTTP body. You can use your favorite
|
accepts and returns JSON in the HTTP body. You can use your favorite
|
||||||
HTTP/REST library for your programming language to use PeerTube. No official
|
HTTP/REST library for your programming language to use PeerTube. No official
|
||||||
SDK is currently provided.
|
SDK is currently provided, but the spec API is fully compatible with
|
||||||
|
[openapi-generator](https://github.com/OpenAPITools/openapi-generator/wiki/API-client-generator-HOWTO)
|
||||||
|
which generates a client SDK in the language of your choice.
|
||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
When you sign up for an account, you are given the possibility to generate
|
When you sign up for an account, you are given the possibility to generate
|
||||||
sessions, and authenticate using this session token. One session token can
|
sessions, and authenticate using this session token. One session token can
|
||||||
currently be used at a time.
|
currently be used at a time.
|
||||||
|
|
||||||
|
# Errors
|
||||||
|
The API uses standard HTTP status codes to indicate the success or failure
|
||||||
|
of the API call. The body of the response will be JSON in the following
|
||||||
|
format.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"code": "unauthorized_request", // example inner error code
|
||||||
|
"error": "Token is invalid." // example exposed error message
|
||||||
|
}
|
||||||
|
```
|
||||||
|
externalDocs:
|
||||||
|
url: https://docs.joinpeertube.org/api.html
|
||||||
tags:
|
tags:
|
||||||
- name: Accounts
|
- name: Accounts
|
||||||
description: >
|
description: >
|
||||||
Using some features of PeerTube require authentication, for which Accounts
|
Using some features of PeerTube require authentication, for which Accounts
|
||||||
|
|
||||||
provide different levels of permission as well as associated user
|
provide different levels of permission as well as associated user
|
||||||
information.
|
information. Accounts also encompass remote accounts discovered across the federation.
|
||||||
|
|
||||||
Accounts also encompass remote accounts discovered across the federation.
|
|
||||||
- name: Config
|
- name: Config
|
||||||
description: >
|
description: >
|
||||||
Each server exposes public information regarding supported videos and
|
Each server exposes public information regarding supported videos and
|
||||||
|
@ -42,23 +56,15 @@ tags:
|
||||||
- name: Job
|
- name: Job
|
||||||
description: >
|
description: >
|
||||||
Jobs are long-running tasks enqueued and processed by the instance
|
Jobs are long-running tasks enqueued and processed by the instance
|
||||||
itself.
|
itself. No additional worker registration is currently available.
|
||||||
|
- name: Server Following
|
||||||
No additional worker registration is currently available.
|
|
||||||
- name: ServerFollowing
|
|
||||||
description: >
|
description: >
|
||||||
Managing servers which the instance interacts with is crucial to the
|
Managing servers which the instance interacts with is crucial to the
|
||||||
concept
|
concept of federation in PeerTube and external video indexation. The PeerTube
|
||||||
|
server then deals with inter-server ActivityPub operations and propagates
|
||||||
of federation in PeerTube and external video indexation. The PeerTube
|
|
||||||
server
|
|
||||||
|
|
||||||
then deals with inter-server ActivityPub operations and propagates
|
|
||||||
|
|
||||||
information across its social graph by posting activities to actors' inbox
|
information across its social graph by posting activities to actors' inbox
|
||||||
|
|
||||||
endpoints.
|
endpoints.
|
||||||
- name: VideoAbuse
|
- name: Video Abuse
|
||||||
description: |
|
description: |
|
||||||
Video abuses deal with reports of local or remote videos alike.
|
Video abuses deal with reports of local or remote videos alike.
|
||||||
- name: Video
|
- name: Video
|
||||||
|
@ -70,16 +76,51 @@ tags:
|
||||||
Videos from other instances federated by the instance (that is, instances
|
Videos from other instances federated by the instance (that is, instances
|
||||||
followed by the instance) can be found via keywords and other criteria of
|
followed by the instance) can be found via keywords and other criteria of
|
||||||
the advanced search.
|
the advanced search.
|
||||||
- name: VideoComment
|
- name: Video Comment
|
||||||
description: >
|
description: >
|
||||||
Operations dealing with comments to a video. Comments are organized in
|
Operations dealing with comments to a video. Comments are organized in
|
||||||
threads.
|
threads.
|
||||||
- name: VideoChannel
|
- name: Video Channel
|
||||||
description: >
|
description: >
|
||||||
Operations dealing with creation, modification and video listing of a
|
Operations dealing with creation, modification and video listing of a
|
||||||
user's
|
user's channels.
|
||||||
|
- name: Video Blacklist
|
||||||
channels.
|
description: >
|
||||||
|
Operations dealing with blacklisting videos (removing them from view and
|
||||||
|
preventing interactions).
|
||||||
|
- name: Video Rate
|
||||||
|
description: >
|
||||||
|
Voting for a video.
|
||||||
|
x-tagGroups:
|
||||||
|
- name: Accounts
|
||||||
|
tags:
|
||||||
|
- Accounts
|
||||||
|
- User
|
||||||
|
- name: Videos
|
||||||
|
tags:
|
||||||
|
- Video
|
||||||
|
- Video Channel
|
||||||
|
- Video Comment
|
||||||
|
- Video Abuse
|
||||||
|
- Video Following
|
||||||
|
- Video Rate
|
||||||
|
- name: Moderation
|
||||||
|
tags:
|
||||||
|
- Video Abuse
|
||||||
|
- Video Blacklist
|
||||||
|
- name: Public Instance Information
|
||||||
|
tags:
|
||||||
|
- Config
|
||||||
|
- Server Following
|
||||||
|
- name: Notifications
|
||||||
|
tags:
|
||||||
|
- Feeds
|
||||||
|
- name: Jobs
|
||||||
|
tags:
|
||||||
|
- Job
|
||||||
|
- name: Search
|
||||||
|
tags:
|
||||||
|
- Search
|
||||||
paths:
|
paths:
|
||||||
'/accounts/{name}':
|
'/accounts/{name}':
|
||||||
get:
|
get:
|
||||||
|
@ -126,6 +167,37 @@ paths:
|
||||||
source: |
|
source: |
|
||||||
# pip install httpie
|
# pip install httpie
|
||||||
http -b GET https://peertube2.cpy.re/api/v1/accounts/{name}/videos
|
http -b GET https://peertube2.cpy.re/api/v1/accounts/{name}/videos
|
||||||
|
- lang: Ruby
|
||||||
|
source: |
|
||||||
|
require 'uri'
|
||||||
|
require 'net/http'
|
||||||
|
|
||||||
|
url = URI("https://peertube2.cpy.re/api/v1/accounts/{name}/videos")
|
||||||
|
|
||||||
|
http = Net::HTTP.new(url.host, url.port)
|
||||||
|
http.use_ssl = true
|
||||||
|
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||||||
|
|
||||||
|
request = Net::HTTP::Post.new(url)
|
||||||
|
request["content-type"] = 'application/json'
|
||||||
|
response = http.request(request)
|
||||||
|
puts response.read_body
|
||||||
|
- lang: Python
|
||||||
|
source: |
|
||||||
|
import http.client
|
||||||
|
|
||||||
|
conn = http.client.HTTPSConnection("https://peertube2.cpy.re/api/v1")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'content-type': "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.request("POST", "/accounts/{name}/videos", None, headers)
|
||||||
|
|
||||||
|
res = conn.getresponse()
|
||||||
|
data = res.read()
|
||||||
|
|
||||||
|
print(data.decode("utf-8"))
|
||||||
/accounts:
|
/accounts:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
@ -144,7 +216,7 @@ paths:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Config
|
- Config
|
||||||
summary: Get the configuration of the server
|
summary: Get the public configuration of the server
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: successful operation
|
description: successful operation
|
||||||
|
@ -152,6 +224,45 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ServerConfig'
|
$ref: '#/components/schemas/ServerConfig'
|
||||||
|
/config/about:
|
||||||
|
get:
|
||||||
|
summary: Get the instance about page content
|
||||||
|
tags:
|
||||||
|
- Config
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
/config/custom:
|
||||||
|
get:
|
||||||
|
summary: Get the runtime configuration of the server
|
||||||
|
tags:
|
||||||
|
- Config
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- admin
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
put:
|
||||||
|
summary: Set the runtime configuration of the server
|
||||||
|
tags:
|
||||||
|
- Config
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- admin
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
delete:
|
||||||
|
summary: Delete the runtime configuration of the server
|
||||||
|
tags:
|
||||||
|
- Config
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- admin
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
'/feeds/videos.{format}':
|
'/feeds/videos.{format}':
|
||||||
get:
|
get:
|
||||||
summary: >-
|
summary: >-
|
||||||
|
@ -223,7 +334,7 @@ paths:
|
||||||
- OAuth2:
|
- OAuth2:
|
||||||
- admin
|
- admin
|
||||||
tags:
|
tags:
|
||||||
- ServerFollowing
|
- Server Following
|
||||||
summary: Unfollow a server by hostname
|
summary: Unfollow a server by hostname
|
||||||
parameters:
|
parameters:
|
||||||
- name: host
|
- name: host
|
||||||
|
@ -238,7 +349,7 @@ paths:
|
||||||
/server/followers:
|
/server/followers:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- ServerFollowing
|
- Server Following
|
||||||
summary: Get followers of the server
|
summary: Get followers of the server
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/start'
|
- $ref: '#/components/parameters/start'
|
||||||
|
@ -256,7 +367,7 @@ paths:
|
||||||
/server/following:
|
/server/following:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- ServerFollowing
|
- Server Following
|
||||||
summary: Get servers followed by the server
|
summary: Get servers followed by the server
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/start'
|
- $ref: '#/components/parameters/start'
|
||||||
|
@ -276,7 +387,7 @@ paths:
|
||||||
- OAuth2:
|
- OAuth2:
|
||||||
- admin
|
- admin
|
||||||
tags:
|
tags:
|
||||||
- ServerFollowing
|
- Server Following
|
||||||
summary: Follow a server
|
summary: Follow a server
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
|
@ -701,6 +812,85 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
$ref: '#/paths/~1users~1me/put/responses/204'
|
$ref: '#/paths/~1users~1me/put/responses/204'
|
||||||
|
'/videos/{id}/watching':
|
||||||
|
put:
|
||||||
|
summary: Indicate progress of in watching the video by its id for a user
|
||||||
|
tags:
|
||||||
|
- Video
|
||||||
|
security:
|
||||||
|
- OAuth2: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/id2'
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserWatchingVideo'
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
$ref: '#/paths/~1users~1me/put/responses/204'
|
||||||
|
/videos/ownership:
|
||||||
|
get:
|
||||||
|
summary: Get list of video ownership changes requests
|
||||||
|
tags:
|
||||||
|
- Video
|
||||||
|
security:
|
||||||
|
- OAuth2: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/id2'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
'/videos/ownership/{id}/accept':
|
||||||
|
post:
|
||||||
|
summary: Refuse ownership change request for video by its id
|
||||||
|
tags:
|
||||||
|
- Video
|
||||||
|
security:
|
||||||
|
- OAuth2: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/id2'
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
$ref: '#/paths/~1users~1me/put/responses/204'
|
||||||
|
'/videos/ownership/{id}/refuse':
|
||||||
|
post:
|
||||||
|
summary: Accept ownership change request for video by its id
|
||||||
|
tags:
|
||||||
|
- Video
|
||||||
|
security:
|
||||||
|
- OAuth2: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/id2'
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
$ref: '#/paths/~1users~1me/put/responses/204'
|
||||||
|
'/videos/{id}/give-ownership':
|
||||||
|
post:
|
||||||
|
summary: Request change of ownership for a video you own, by its id
|
||||||
|
tags:
|
||||||
|
- Video
|
||||||
|
security:
|
||||||
|
- OAuth2: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/id2'
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/x-www-form-urlencoded:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
$ref: '#/paths/~1users~1me/put/responses/204'
|
||||||
|
'400':
|
||||||
|
description: 'Changing video ownership to a remote account is not supported yet'
|
||||||
/videos/upload:
|
/videos/upload:
|
||||||
post:
|
post:
|
||||||
summary: Upload a video file with its metadata
|
summary: Upload a video file with its metadata
|
||||||
|
@ -771,7 +961,6 @@ paths:
|
||||||
- videofile
|
- videofile
|
||||||
- channelId
|
- channelId
|
||||||
- name
|
- name
|
||||||
- privacy
|
|
||||||
x-code-samples:
|
x-code-samples:
|
||||||
- lang: Shell
|
- lang: Shell
|
||||||
source: |
|
source: |
|
||||||
|
@ -781,7 +970,6 @@ paths:
|
||||||
PASSWORD="<your_password>"
|
PASSWORD="<your_password>"
|
||||||
FILE_PATH="<your_file_path>"
|
FILE_PATH="<your_file_path>"
|
||||||
CHANNEL_ID="<your_channel_id>"
|
CHANNEL_ID="<your_channel_id>"
|
||||||
PRIVACY="1" # public: 1, unlisted: 2, private: 3
|
|
||||||
NAME="<video_name>"
|
NAME="<video_name>"
|
||||||
|
|
||||||
API_PATH="https://peertube2.cpy.re/api/v1"
|
API_PATH="https://peertube2.cpy.re/api/v1"
|
||||||
|
@ -798,7 +986,6 @@ paths:
|
||||||
videofile@$FILE_PATH \
|
videofile@$FILE_PATH \
|
||||||
channelId=$CHANNEL_ID \
|
channelId=$CHANNEL_ID \
|
||||||
name=$NAME \
|
name=$NAME \
|
||||||
privacy=$PRIVACY \
|
|
||||||
"Authorization:Bearer $token"
|
"Authorization:Bearer $token"
|
||||||
/videos/abuse:
|
/videos/abuse:
|
||||||
get:
|
get:
|
||||||
|
@ -806,7 +993,7 @@ paths:
|
||||||
security:
|
security:
|
||||||
- OAuth2: []
|
- OAuth2: []
|
||||||
tags:
|
tags:
|
||||||
- VideoAbuse
|
- Video Abuse
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/start'
|
- $ref: '#/components/parameters/start'
|
||||||
- $ref: '#/components/parameters/count'
|
- $ref: '#/components/parameters/count'
|
||||||
|
@ -826,7 +1013,7 @@ paths:
|
||||||
security:
|
security:
|
||||||
- OAuth2: []
|
- OAuth2: []
|
||||||
tags:
|
tags:
|
||||||
- VideoAbuse
|
- Video Abuse
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/id2'
|
- $ref: '#/components/parameters/id2'
|
||||||
responses:
|
responses:
|
||||||
|
@ -840,7 +1027,7 @@ paths:
|
||||||
- admin
|
- admin
|
||||||
- moderator
|
- moderator
|
||||||
tags:
|
tags:
|
||||||
- VideoBlacklist
|
- Video Blacklist
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/id2'
|
- $ref: '#/components/parameters/id2'
|
||||||
responses:
|
responses:
|
||||||
|
@ -853,7 +1040,7 @@ paths:
|
||||||
- admin
|
- admin
|
||||||
- moderator
|
- moderator
|
||||||
tags:
|
tags:
|
||||||
- VideoBlacklist
|
- Video Blacklist
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/id2'
|
- $ref: '#/components/parameters/id2'
|
||||||
responses:
|
responses:
|
||||||
|
@ -867,7 +1054,7 @@ paths:
|
||||||
- admin
|
- admin
|
||||||
- moderator
|
- moderator
|
||||||
tags:
|
tags:
|
||||||
- VideoBlacklist
|
- Video Blacklist
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/start'
|
- $ref: '#/components/parameters/start'
|
||||||
- $ref: '#/components/parameters/count'
|
- $ref: '#/components/parameters/count'
|
||||||
|
@ -885,7 +1072,7 @@ paths:
|
||||||
get:
|
get:
|
||||||
summary: Get list of video channels
|
summary: Get list of video channels
|
||||||
tags:
|
tags:
|
||||||
- VideoChannel
|
- Video Channel
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/start'
|
- $ref: '#/components/parameters/start'
|
||||||
- $ref: '#/components/parameters/count'
|
- $ref: '#/components/parameters/count'
|
||||||
|
@ -904,7 +1091,7 @@ paths:
|
||||||
security:
|
security:
|
||||||
- OAuth2: []
|
- OAuth2: []
|
||||||
tags:
|
tags:
|
||||||
- VideoChannel
|
- Video Channel
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
$ref: '#/paths/~1users~1me/put/responses/204'
|
$ref: '#/paths/~1users~1me/put/responses/204'
|
||||||
|
@ -914,7 +1101,7 @@ paths:
|
||||||
get:
|
get:
|
||||||
summary: Get a video channel by its id
|
summary: Get a video channel by its id
|
||||||
tags:
|
tags:
|
||||||
- VideoChannel
|
- Video Channel
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/id3'
|
- $ref: '#/components/parameters/id3'
|
||||||
responses:
|
responses:
|
||||||
|
@ -929,7 +1116,7 @@ paths:
|
||||||
security:
|
security:
|
||||||
- OAuth2: []
|
- OAuth2: []
|
||||||
tags:
|
tags:
|
||||||
- VideoChannel
|
- Video Channel
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/id3'
|
- $ref: '#/components/parameters/id3'
|
||||||
responses:
|
responses:
|
||||||
|
@ -942,7 +1129,7 @@ paths:
|
||||||
security:
|
security:
|
||||||
- OAuth2: []
|
- OAuth2: []
|
||||||
tags:
|
tags:
|
||||||
- VideoChannel
|
- Video Channel
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/id3'
|
- $ref: '#/components/parameters/id3'
|
||||||
responses:
|
responses:
|
||||||
|
@ -952,7 +1139,7 @@ paths:
|
||||||
get:
|
get:
|
||||||
summary: Get videos of a video channel by its id
|
summary: Get videos of a video channel by its id
|
||||||
tags:
|
tags:
|
||||||
- VideoChannel
|
- Video Channel
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/id3'
|
- $ref: '#/components/parameters/id3'
|
||||||
responses:
|
responses:
|
||||||
|
@ -966,7 +1153,7 @@ paths:
|
||||||
get:
|
get:
|
||||||
summary: Get video channels of an account by its name
|
summary: Get video channels of an account by its name
|
||||||
tags:
|
tags:
|
||||||
- VideoChannel
|
- Video Channel
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/name'
|
- $ref: '#/components/parameters/name'
|
||||||
responses:
|
responses:
|
||||||
|
@ -982,7 +1169,7 @@ paths:
|
||||||
get:
|
get:
|
||||||
summary: Get the comment threads of a video by its id
|
summary: Get the comment threads of a video by its id
|
||||||
tags:
|
tags:
|
||||||
- VideoComment
|
- Video Comment
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/id2'
|
- $ref: '#/components/parameters/id2'
|
||||||
- $ref: '#/components/parameters/start'
|
- $ref: '#/components/parameters/start'
|
||||||
|
@ -1000,7 +1187,7 @@ paths:
|
||||||
security:
|
security:
|
||||||
- OAuth2: []
|
- OAuth2: []
|
||||||
tags:
|
tags:
|
||||||
- VideoComment
|
- Video Comment
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/id2'
|
- $ref: '#/components/parameters/id2'
|
||||||
responses:
|
responses:
|
||||||
|
@ -1014,7 +1201,7 @@ paths:
|
||||||
get:
|
get:
|
||||||
summary: 'Get the comment thread by its id, of a video by its id'
|
summary: 'Get the comment thread by its id, of a video by its id'
|
||||||
tags:
|
tags:
|
||||||
- VideoComment
|
- Video Comment
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/id2'
|
- $ref: '#/components/parameters/id2'
|
||||||
- name: threadId
|
- name: threadId
|
||||||
|
@ -1036,7 +1223,7 @@ paths:
|
||||||
security:
|
security:
|
||||||
- OAuth2: []
|
- OAuth2: []
|
||||||
tags:
|
tags:
|
||||||
- VideoComment
|
- Video Comment
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/id2'
|
- $ref: '#/components/parameters/id2'
|
||||||
- $ref: '#/components/parameters/commentId'
|
- $ref: '#/components/parameters/commentId'
|
||||||
|
@ -1052,7 +1239,7 @@ paths:
|
||||||
security:
|
security:
|
||||||
- OAuth2: []
|
- OAuth2: []
|
||||||
tags:
|
tags:
|
||||||
- VideoComment
|
- Video Comment
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/id2'
|
- $ref: '#/components/parameters/id2'
|
||||||
- $ref: '#/components/parameters/commentId'
|
- $ref: '#/components/parameters/commentId'
|
||||||
|
@ -1065,7 +1252,7 @@ paths:
|
||||||
security:
|
security:
|
||||||
- OAuth2: []
|
- OAuth2: []
|
||||||
tags:
|
tags:
|
||||||
- VideoRate
|
- Video Rate
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/id2'
|
- $ref: '#/components/parameters/id2'
|
||||||
responses:
|
responses:
|
||||||
|
@ -1096,8 +1283,12 @@ paths:
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Video'
|
$ref: '#/components/schemas/Video'
|
||||||
servers:
|
servers:
|
||||||
|
- url: 'https://peertube.cpy.re/api/v1'
|
||||||
|
description: Live Test Server (live data - stable version)
|
||||||
- url: 'https://peertube2.cpy.re/api/v1'
|
- url: 'https://peertube2.cpy.re/api/v1'
|
||||||
description: Live Server
|
description: Live Test Server (live data - bleeding edge version)
|
||||||
|
- url: 'https://peertube3.cpy.re/api/v1'
|
||||||
|
description: Live Test Server (live data - bleeding edge version)
|
||||||
components:
|
components:
|
||||||
parameters:
|
parameters:
|
||||||
start:
|
start:
|
||||||
|
@ -1417,6 +1608,10 @@ components:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/VideoChannel'
|
$ref: '#/components/schemas/VideoChannel'
|
||||||
|
UserWatchingVideo:
|
||||||
|
properties:
|
||||||
|
currentTime:
|
||||||
|
type: number
|
||||||
ServerConfig:
|
ServerConfig:
|
||||||
properties:
|
properties:
|
||||||
signup:
|
signup:
|
||||||
|
|
Loading…
Reference in New Issue