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 {
|
||||
sortBy,
|
||||
durationToString,
|
||||
|
@ -135,5 +139,6 @@ export {
|
|||
immutableAssign,
|
||||
objectToFormData,
|
||||
lineFeedToHtml,
|
||||
removeElementFromArray
|
||||
removeElementFromArray,
|
||||
scrollToTop
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
Create an account
|
||||
</div>
|
||||
|
||||
<div *ngIf="info" class="alert alert-info">{{ info }}</div>
|
||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||
|
||||
<div class="d-flex justify-content-left flex-wrap">
|
||||
|
@ -59,7 +60,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<input type="submit" i18n-value value="Signup" [disabled]="!form.valid">
|
||||
<input type="submit" i18n-value value="Signup" [disabled]="!form.valid || signupDone">
|
||||
</form>
|
||||
|
||||
<div>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'
|
|||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { UserCreate } 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 { 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' ]
|
||||
})
|
||||
export class SignupComponent extends FormReactive implements OnInit {
|
||||
info: string = null
|
||||
error: string = null
|
||||
signupDone = false
|
||||
|
||||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
private authService: AuthService,
|
||||
private userValidatorsService: UserValidatorsService,
|
||||
private notificationsService: NotificationsService,
|
||||
private userService: UserService,
|
||||
|
@ -50,18 +53,27 @@ export class SignupComponent extends FormReactive implements OnInit {
|
|||
|
||||
this.userService.signup(userCreate).subscribe(
|
||||
() => {
|
||||
this.signupDone = true
|
||||
|
||||
if (this.requiresEmailVerification) {
|
||||
this.notificationsService.alert(
|
||||
this.i18n('Welcome'),
|
||||
this.i18n('Please check your email to verify your account and complete signup.')
|
||||
)
|
||||
} else {
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('Registration for {{username}} complete.', { username: userCreate.username })
|
||||
)
|
||||
this.info = this.i18n('Welcome! Now please check your emails to verify your account and complete signup.')
|
||||
return
|
||||
}
|
||||
this.redirectService.redirectToHomepage()
|
||||
|
||||
// Auto login
|
||||
this.authService.login(userCreate.username, userCreate.password)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('You are now logged in as {{username}}!', { username: userCreate.username })
|
||||
)
|
||||
|
||||
this.redirectService.redirectToHomepage()
|
||||
},
|
||||
|
||||
err => this.error = err.message
|
||||
)
|
||||
},
|
||||
|
||||
err => this.error = err.message
|
||||
|
|
|
@ -60,6 +60,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
|
|||
hide () {
|
||||
this.closingModal = true
|
||||
this.openedModal.close()
|
||||
this.form.reset()
|
||||
}
|
||||
|
||||
isReplacingExistingCaption () {
|
||||
|
|
|
@ -45,7 +45,12 @@
|
|||
</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.
|
||||
</div>
|
||||
|
||||
|
|
|
@ -7,6 +7,14 @@ $width-size: 190px;
|
|||
@include peertube-select-container($width-size);
|
||||
}
|
||||
|
||||
.alert.alert-danger {
|
||||
text-align: center;
|
||||
|
||||
& > div {
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.import-video-torrent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -12,6 +12,7 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
|
|||
import { FormValidatorService } from '@app/shared'
|
||||
import { VideoCaptionService } from '@app/shared/video-caption'
|
||||
import { VideoImportService } from '@app/shared/video-import'
|
||||
import { scrollToTop } from '@app/shared/misc/utils'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-import-torrent',
|
||||
|
@ -23,9 +24,9 @@ import { VideoImportService } from '@app/shared/video-import'
|
|||
})
|
||||
export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate {
|
||||
@Output() firstStepDone = new EventEmitter<string>()
|
||||
@Output() firstStepError = new EventEmitter<void>()
|
||||
@ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement>
|
||||
|
||||
videoFileName: string
|
||||
magnetUri = ''
|
||||
|
||||
isImportingVideo = false
|
||||
|
@ -33,6 +34,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
|
|||
isUpdatingVideo = false
|
||||
|
||||
video: VideoEdit
|
||||
error: string
|
||||
|
||||
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
|
||||
|
||||
|
@ -104,6 +106,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
|
|||
err => {
|
||||
this.loadingBar.complete()
|
||||
this.isImportingVideo = false
|
||||
this.firstStepError.emit()
|
||||
this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
}
|
||||
)
|
||||
|
@ -129,8 +132,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
|
|||
},
|
||||
|
||||
err => {
|
||||
this.isUpdatingVideo = false
|
||||
this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
this.error = err.message
|
||||
scrollToTop()
|
||||
console.error(err)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -37,7 +37,13 @@
|
|||
</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.
|
||||
</div>
|
||||
|
||||
|
|
|
@ -7,6 +7,14 @@ $width-size: 190px;
|
|||
@include peertube-select-container($width-size);
|
||||
}
|
||||
|
||||
.alert.alert-danger {
|
||||
text-align: center;
|
||||
|
||||
& > div {
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.import-video-url {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -12,6 +12,7 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
|
|||
import { FormValidatorService } from '@app/shared'
|
||||
import { VideoCaptionService } from '@app/shared/video-caption'
|
||||
import { VideoImportService } from '@app/shared/video-import'
|
||||
import { scrollToTop } from '@app/shared/misc/utils'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-import-url',
|
||||
|
@ -23,15 +24,16 @@ import { VideoImportService } from '@app/shared/video-import'
|
|||
})
|
||||
export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate {
|
||||
@Output() firstStepDone = new EventEmitter<string>()
|
||||
@Output() firstStepError = new EventEmitter<void>()
|
||||
|
||||
targetUrl = ''
|
||||
videoFileName: string
|
||||
|
||||
isImportingVideo = false
|
||||
hasImportedVideo = false
|
||||
isUpdatingVideo = false
|
||||
|
||||
video: VideoEdit
|
||||
error: string
|
||||
|
||||
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
|
||||
|
||||
|
@ -96,6 +98,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
|
|||
err => {
|
||||
this.loadingBar.complete()
|
||||
this.isImportingVideo = false
|
||||
this.firstStepError.emit()
|
||||
this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
}
|
||||
)
|
||||
|
@ -121,8 +124,8 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
|
|||
},
|
||||
|
||||
err => {
|
||||
this.isUpdatingVideo = false
|
||||
this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
this.error = err.message
|
||||
scrollToTop()
|
||||
console.error(err)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -21,6 +21,7 @@ export abstract class VideoSend extends FormReactive implements OnInit {
|
|||
firstStepChannelId = 0
|
||||
|
||||
abstract firstStepDone: EventEmitter<string>
|
||||
abstract firstStepError: EventEmitter<void>
|
||||
protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy
|
||||
|
||||
protected loadingBar: LoadingBarService
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isUploadingVideo" class="upload-progress-cancel">
|
||||
<div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel">
|
||||
<p-progressBar
|
||||
[value]="videoUploadPercents"
|
||||
[ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }"
|
||||
|
@ -37,6 +37,11 @@
|
|||
<input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" />
|
||||
</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 -->
|
||||
<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
|
||||
<my-video-edit
|
||||
|
@ -55,4 +60,4 @@
|
|||
<input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
|
|
|
@ -5,6 +5,14 @@
|
|||
@include peertube-select-container(190px);
|
||||
}
|
||||
|
||||
.alert.alert-danger {
|
||||
text-align: center;
|
||||
|
||||
& > div {
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-video {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -82,4 +90,4 @@
|
|||
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { FormValidatorService, UserService } from '@app/shared'
|
||||
import { VideoCaptionService } from '@app/shared/video-caption'
|
||||
import { scrollToTop } from '@app/shared/misc/utils'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-upload',
|
||||
|
@ -25,6 +26,7 @@ import { VideoCaptionService } from '@app/shared/video-caption'
|
|||
})
|
||||
export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
@Output() firstStepDone = new EventEmitter<string>()
|
||||
@Output() firstStepError = new EventEmitter<void>()
|
||||
@ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
|
||||
|
||||
// So that it can be accessed in the template
|
||||
|
@ -43,6 +45,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
uuid: ''
|
||||
}
|
||||
|
||||
error: string
|
||||
|
||||
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
|
||||
|
||||
constructor (
|
||||
|
@ -201,6 +205,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
this.isUploadingVideo = false
|
||||
this.videoUploadPercents = 0
|
||||
this.videoUploadObservable = null
|
||||
this.firstStepError.emit()
|
||||
this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
}
|
||||
)
|
||||
|
@ -235,8 +240,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
},
|
||||
|
||||
err => {
|
||||
this.isUpdatingVideo = false
|
||||
this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
this.error = err.message
|
||||
scrollToTop()
|
||||
console.error(err)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -6,24 +6,24 @@
|
|||
|
||||
<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 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>
|
||||
</ngb-tab>
|
||||
|
||||
<ngb-tab *ngIf="isVideoImportHttpEnabled()">
|
||||
<ng-template ngbTabTitle><span i18n>Import with URL</span></ng-template>
|
||||
<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>
|
||||
</ngb-tab>
|
||||
|
||||
<ngb-tab *ngIf="isVideoImportTorrentEnabled()">
|
||||
<ng-template ngbTabTitle><span i18n>Import with torrent</span></ng-template>
|
||||
<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>
|
||||
</ngb-tab>
|
||||
</ngb-tabset>
|
||||
|
|
|
@ -27,6 +27,11 @@ export class VideoAddComponent implements CanComponentDeactivate {
|
|||
this.videoName = videoName
|
||||
}
|
||||
|
||||
onError () {
|
||||
this.videoName = undefined
|
||||
this.secondStepType = undefined
|
||||
}
|
||||
|
||||
canDeactivate () {
|
||||
if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate()
|
||||
if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate()
|
||||
|
|
|
@ -114,7 +114,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
)
|
||||
.pipe(
|
||||
// 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 ]) => {
|
||||
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()
|
||||
if (muted !== undefined) this.player.muted(muted)
|
||||
|
||||
this.player.duration(options.videoDuration)
|
||||
|
||||
this.initializePlayer()
|
||||
this.runTorrentInfoScheduler()
|
||||
this.runViewAdd()
|
||||
|
@ -302,6 +304,9 @@ class PeerTubePlugin extends Plugin {
|
|||
|
||||
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 }
|
||||
renderVideo(torrent.files[ 0 ], this.playerElement, renderVideoOptions, (err, renderer) => {
|
||||
this.renderer = renderer
|
||||
|
|
|
@ -171,7 +171,7 @@ $setting-transition-easing: ease-out;
|
|||
left: 8px;
|
||||
content: ' ';
|
||||
margin-top: 1px;
|
||||
background-image: url('#{$assets-path}/player/images/tick.svg');
|
||||
background-image: url('#{$assets-path}/player/images/tick-white.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -197,4 +197,4 @@ $setting-transition-easing: ease-out;
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,10 @@ log:
|
|||
level: 'info' # debug/info/warning/error
|
||||
|
||||
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
|
||||
anonymous: false
|
||||
|
||||
|
|
|
@ -59,7 +59,10 @@ log:
|
|||
level: 'info' # debug/info/warning/error
|
||||
|
||||
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
|
||||
anonymous: false
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ import {
|
|||
import { VideoCaptionModel } from '../../models/video/video-caption'
|
||||
import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy'
|
||||
import { getServerActor } from '../../helpers/utils'
|
||||
import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
|
||||
|
||||
const activityPubClientRouter = express.Router()
|
||||
|
||||
|
@ -164,6 +165,8 @@ function getAccountVideoRate (rateType: VideoRateType) {
|
|||
async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
const video: VideoModel = res.locals.video
|
||||
|
||||
if (video.isOwned() === false) return res.redirect(video.url)
|
||||
|
||||
// We need captions to render AP object
|
||||
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) {
|
||||
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)
|
||||
|
||||
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) {
|
||||
const videoComment: VideoCommentModel = res.locals.videoComment
|
||||
|
||||
if (videoComment.isOwned() === false) return res.redirect(videoComment.url)
|
||||
|
||||
const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
|
||||
const isPublic = true // Comments are always public
|
||||
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) {
|
||||
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 audience = getAudience(serverActor)
|
||||
|
@ -288,7 +298,7 @@ async function actorFollowing (req: express.Request, actor: ActorModel) {
|
|||
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) {
|
||||
|
@ -296,7 +306,7 @@ async function actorFollowers (req: express.Request, actor: ActorModel) {
|
|||
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) {
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
asyncMiddleware,
|
||||
asyncRetryTransactionMiddleware,
|
||||
authenticate,
|
||||
checkVideoFollowConstraints,
|
||||
commonVideosFiltersValidator,
|
||||
optionalAuthenticate,
|
||||
paginationValidator,
|
||||
|
@ -123,6 +124,7 @@ videosRouter.get('/:id/description',
|
|||
videosRouter.get('/:id',
|
||||
optionalAuthenticate,
|
||||
asyncMiddleware(videosGetValidator),
|
||||
asyncMiddleware(checkVideoFollowConstraints),
|
||||
getVideo
|
||||
)
|
||||
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>>
|
||||
async function activityPubCollectionPagination (url: string, handler: ActivityPubCollectionPaginationHandler, page?: any) {
|
||||
async function activityPubCollectionPagination (baseUrl: string, handler: ActivityPubCollectionPaginationHandler, page?: any) {
|
||||
if (!page || !validator.isInt(page)) {
|
||||
// We just display the first page URL, we only need the total items
|
||||
const result = await handler(0, 1)
|
||||
|
||||
return {
|
||||
id: url,
|
||||
id: baseUrl,
|
||||
type: 'OrderedCollection',
|
||||
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
|
||||
if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) {
|
||||
next = url + '?page=' + (page + 1)
|
||||
next = baseUrl + '?page=' + (page + 1)
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
prev = url + '?page=' + (page - 1)
|
||||
prev = baseUrl + '?page=' + (page - 1)
|
||||
}
|
||||
|
||||
return {
|
||||
id: url + '?page=' + page,
|
||||
id: baseUrl + '?page=' + page,
|
||||
type: 'OrderedCollectionPage',
|
||||
prev,
|
||||
next,
|
||||
partOf: url,
|
||||
partOf: baseUrl,
|
||||
orderedItems: result.data,
|
||||
totalItems: result.total
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as Bluebird from 'bluebird'
|
|||
import { createWriteStream } from 'fs-extra'
|
||||
import * as request from 'request'
|
||||
import { ACTIVITY_PUB } from '../initializers'
|
||||
import { processImage } from './image-utils'
|
||||
|
||||
function doRequest <T> (
|
||||
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 {
|
||||
doRequest,
|
||||
doRequestAndSaveToFile
|
||||
doRequestAndSaveToFile,
|
||||
downloadImage
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp
|
|||
import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
|
||||
import { logger } from '../../helpers/logger'
|
||||
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 { 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 { ActorModel } from '../../models/activitypub/actor'
|
||||
import { AvatarModel } from '../../models/avatar/avatar'
|
||||
|
@ -180,10 +180,7 @@ async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
|
|||
const avatarName = uuidv4() + extension
|
||||
const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
|
||||
|
||||
await doRequestAndSaveToFile({
|
||||
method: 'GET',
|
||||
uri: actorJSON.icon.url
|
||||
}, destPath)
|
||||
await downloadImage(actorJSON.icon.url, destPath, AVATARS_SIZE)
|
||||
|
||||
return avatarName
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validat
|
|||
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
|
||||
import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
|
||||
import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers'
|
||||
import { doRequest, downloadImage } from '../../helpers/requests'
|
||||
import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_MIMETYPE_EXT } from '../../initializers'
|
||||
import { ActorModel } from '../../models/activitypub/actor'
|
||||
import { TagModel } from '../../models/video/tag'
|
||||
import { VideoModel } from '../../models/video/video'
|
||||
|
@ -97,11 +97,7 @@ function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject)
|
|||
const thumbnailName = video.getThumbnailName()
|
||||
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
uri: icon.url
|
||||
}
|
||||
return doRequestAndSaveToFile(options, thumbnailPath)
|
||||
return downloadImage(icon.url, thumbnailPath, THUMBNAILS_SIZE)
|
||||
}
|
||||
|
||||
function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
|
||||
|
|
|
@ -6,8 +6,8 @@ import { VideoImportState } from '../../../../shared/models/videos'
|
|||
import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
|
||||
import { extname, join } from 'path'
|
||||
import { VideoFileModel } from '../../../models/video/video-file'
|
||||
import { CONFIG, sequelizeTypescript, VIDEO_IMPORT_TIMEOUT } from '../../../initializers'
|
||||
import { doRequestAndSaveToFile } from '../../../helpers/requests'
|
||||
import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers'
|
||||
import { doRequestAndSaveToFile, downloadImage } from '../../../helpers/requests'
|
||||
import { VideoState } from '../../../../shared'
|
||||
import { JobQueue } from '../index'
|
||||
import { federateVideoIfNeeded } from '../../activitypub'
|
||||
|
@ -133,7 +133,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
|
|||
videoId: videoImport.videoId
|
||||
}
|
||||
videoFile = new VideoFileModel(videoFileData)
|
||||
// Import if the import fails, to clean files
|
||||
// To clean files if the import fails
|
||||
videoImport.Video.VideoFiles = [ videoFile ]
|
||||
|
||||
// Move file
|
||||
|
@ -145,7 +145,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
|
|||
if (options.downloadThumbnail) {
|
||||
if (options.thumbnailUrl) {
|
||||
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 {
|
||||
await videoImport.Video.createThumbnail(videoFile)
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
|
|||
if (options.downloadPreview) {
|
||||
if (options.thumbnailUrl) {
|
||||
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 {
|
||||
await videoImport.Video.createPreview(videoFile)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ function cacheRoute (lifetimeArg: string | number) {
|
|||
logger.debug('No cached results for route %s.', req.originalUrl)
|
||||
|
||||
const sendSave = res.send.bind(res)
|
||||
const redirectSave = res.redirect.bind(res)
|
||||
|
||||
res.send = (body) => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 400) {
|
||||
|
@ -38,6 +39,12 @@ function cacheRoute (lifetimeArg: string | number) {
|
|||
return sendSave(body)
|
||||
}
|
||||
|
||||
res.redirect = url => {
|
||||
done()
|
||||
|
||||
return redirectSave(url)
|
||||
}
|
||||
|
||||
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) {
|
||||
if (req.header('authorization')) return authenticate(req, res, next)
|
||||
|
||||
res.locals.authenticated = false
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
|
@ -53,6 +68,7 @@ function token (req: express.Request, res: express.Response, next: express.NextF
|
|||
|
||||
export {
|
||||
authenticate,
|
||||
authenticatePromiseIfNeeded,
|
||||
optionalAuthenticate,
|
||||
token
|
||||
}
|
||||
|
|
|
@ -31,8 +31,8 @@ import {
|
|||
} from '../../../helpers/custom-validators/videos'
|
||||
import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { CONSTRAINTS_FIELDS } from '../../../initializers'
|
||||
import { authenticate } from '../../oauth'
|
||||
import { CONFIG, CONSTRAINTS_FIELDS } from '../../../initializers'
|
||||
import { authenticatePromiseIfNeeded } from '../../oauth'
|
||||
import { areValidationErrors } from '../utils'
|
||||
import { cleanUpReqFiles } from '../../../helpers/express-utils'
|
||||
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 { VideoFetchType } from '../../../helpers/video'
|
||||
import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
|
||||
import { getServerActor } from '../../../helpers/utils'
|
||||
|
||||
const videosAddValidator = getCommonVideoAttributes().concat([
|
||||
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) => {
|
||||
return [
|
||||
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
|
||||
|
@ -141,17 +167,20 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => {
|
|||
|
||||
// Video private or blacklisted
|
||||
if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
|
||||
return authenticate(req, res, () => {
|
||||
const user: UserModel = res.locals.oauth.token.User
|
||||
await authenticatePromiseIfNeeded(req, res)
|
||||
|
||||
// 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)) {
|
||||
return res.status(403)
|
||||
.json({ error: 'Cannot get this private or blacklisted video.' })
|
||||
}
|
||||
const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : null
|
||||
|
||||
return next()
|
||||
})
|
||||
// Only the owner or a user that have blacklist rights can see the video
|
||||
if (
|
||||
!user ||
|
||||
(video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
|
||||
) {
|
||||
return res.status(403)
|
||||
.json({ error: 'Cannot get this private or blacklisted video.' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
// Video is public, anyone can access it
|
||||
|
@ -376,6 +405,7 @@ export {
|
|||
videosAddValidator,
|
||||
videosUpdateValidator,
|
||||
videosGetValidator,
|
||||
checkVideoFollowConstraints,
|
||||
videosCustomGetValidator,
|
||||
videosRemoveValidator,
|
||||
|
||||
|
@ -393,6 +423,8 @@ export {
|
|||
function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
|
||||
if (req.body.scheduleUpdate) {
|
||||
if (!req.body.scheduleUpdate.updateAt) {
|
||||
logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
|
||||
|
||||
res.status(400)
|
||||
.json({ error: 'Schedule update at is mandatory.' })
|
||||
|
||||
|
|
|
@ -509,12 +509,12 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
|
|||
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)
|
||||
|
||||
return {
|
||||
data: urls,
|
||||
total: parseInt(total, 10)
|
||||
total: dataTotal ? parseInt(dataTotal.total, 10) : 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -117,8 +117,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
|
|||
|
||||
@BeforeDestroy
|
||||
static async removeFile (instance: VideoRedundancyModel) {
|
||||
// Not us
|
||||
if (!instance.strategy) return
|
||||
if (!instance.isOwned()) return
|
||||
|
||||
const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId)
|
||||
|
||||
|
@ -404,6 +403,10 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
|
|||
}))
|
||||
}
|
||||
|
||||
isOwned () {
|
||||
return !!this.strategy
|
||||
}
|
||||
|
||||
toActivityPubObject (): CacheFileObject {
|
||||
return {
|
||||
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
|
||||
static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
|
||||
const serverActor = await getServerActor()
|
||||
|
|
|
@ -14,11 +14,13 @@ import {
|
|||
setAccessTokensToServers,
|
||||
userLogin
|
||||
} from '../../../../shared/utils'
|
||||
|
||||
import {
|
||||
checkBadCountPagination,
|
||||
checkBadSortPagination,
|
||||
checkBadStartPagination
|
||||
} from '../../../../shared/utils/requests/check-api-params'
|
||||
import { waitJobs } from '../../../../shared/utils/server/jobs'
|
||||
|
||||
describe('Test user subscriptions API validators', function () {
|
||||
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 () {
|
||||
this.timeout(20000)
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
|
@ -152,6 +156,8 @@ describe('Test user subscriptions API validators', function () {
|
|||
fields: { uri: 'user1_channel@localhost:9001' },
|
||||
statusCodeExpected: 204
|
||||
})
|
||||
|
||||
await waitJobs([ server ])
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -17,9 +17,10 @@ import {
|
|||
viewVideo,
|
||||
wait,
|
||||
waitUntilLog,
|
||||
checkVideoFilesWereRemoved, removeVideo
|
||||
checkVideoFilesWereRemoved, removeVideo, getVideoWithToken
|
||||
} from '../../../../shared/utils'
|
||||
import { waitJobs } from '../../../../shared/utils/server/jobs'
|
||||
|
||||
import * as magnetUtil from 'magnet-uri'
|
||||
import { updateRedundancy } from '../../../../shared/utils/server/redundancy'
|
||||
import { ActorFollow } from '../../../../shared/models/actors'
|
||||
|
@ -93,7 +94,8 @@ async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: str
|
|||
|
||||
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
|
||||
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 './email'
|
||||
import './follow-constraints'
|
||||
import './follows'
|
||||
import './handle-down'
|
||||
import './jobs'
|
||||
|
|
|
@ -10,27 +10,41 @@ info:
|
|||
url: 'https://github.com/Chocobozzz/PeerTube/blob/master/LICENSE'
|
||||
x-logo:
|
||||
url: 'https://joinpeertube.org/img/brand.png'
|
||||
altText: PeerTube Project Homepage
|
||||
description: |
|
||||
# Introduction
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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:
|
||||
- name: Accounts
|
||||
description: >
|
||||
Using some features of PeerTube require authentication, for which Accounts
|
||||
|
||||
provide different levels of permission as well as associated user
|
||||
information.
|
||||
|
||||
Accounts also encompass remote accounts discovered across the federation.
|
||||
information. Accounts also encompass remote accounts discovered across the federation.
|
||||
- name: Config
|
||||
description: >
|
||||
Each server exposes public information regarding supported videos and
|
||||
|
@ -42,23 +56,15 @@ tags:
|
|||
- name: Job
|
||||
description: >
|
||||
Jobs are long-running tasks enqueued and processed by the instance
|
||||
itself.
|
||||
|
||||
No additional worker registration is currently available.
|
||||
- name: ServerFollowing
|
||||
itself. No additional worker registration is currently available.
|
||||
- name: Server Following
|
||||
description: >
|
||||
Managing servers which the instance interacts with is crucial to the
|
||||
concept
|
||||
|
||||
of federation in PeerTube and external video indexation. The PeerTube
|
||||
server
|
||||
|
||||
then deals with inter-server ActivityPub operations and propagates
|
||||
|
||||
concept 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
|
||||
|
||||
endpoints.
|
||||
- name: VideoAbuse
|
||||
- name: Video Abuse
|
||||
description: |
|
||||
Video abuses deal with reports of local or remote videos alike.
|
||||
- name: Video
|
||||
|
@ -70,16 +76,51 @@ tags:
|
|||
Videos from other instances federated by the instance (that is, instances
|
||||
followed by the instance) can be found via keywords and other criteria of
|
||||
the advanced search.
|
||||
- name: VideoComment
|
||||
- name: Video Comment
|
||||
description: >
|
||||
Operations dealing with comments to a video. Comments are organized in
|
||||
threads.
|
||||
- name: VideoChannel
|
||||
- name: Video Channel
|
||||
description: >
|
||||
Operations dealing with creation, modification and video listing of a
|
||||
user's
|
||||
|
||||
channels.
|
||||
user's channels.
|
||||
- name: Video Blacklist
|
||||
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:
|
||||
'/accounts/{name}':
|
||||
get:
|
||||
|
@ -126,6 +167,37 @@ paths:
|
|||
source: |
|
||||
# pip install httpie
|
||||
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:
|
||||
get:
|
||||
tags:
|
||||
|
@ -144,7 +216,7 @@ paths:
|
|||
get:
|
||||
tags:
|
||||
- Config
|
||||
summary: Get the configuration of the server
|
||||
summary: Get the public configuration of the server
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
|
@ -152,6 +224,45 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$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}':
|
||||
get:
|
||||
summary: >-
|
||||
|
@ -223,7 +334,7 @@ paths:
|
|||
- OAuth2:
|
||||
- admin
|
||||
tags:
|
||||
- ServerFollowing
|
||||
- Server Following
|
||||
summary: Unfollow a server by hostname
|
||||
parameters:
|
||||
- name: host
|
||||
|
@ -238,7 +349,7 @@ paths:
|
|||
/server/followers:
|
||||
get:
|
||||
tags:
|
||||
- ServerFollowing
|
||||
- Server Following
|
||||
summary: Get followers of the server
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/start'
|
||||
|
@ -256,7 +367,7 @@ paths:
|
|||
/server/following:
|
||||
get:
|
||||
tags:
|
||||
- ServerFollowing
|
||||
- Server Following
|
||||
summary: Get servers followed by the server
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/start'
|
||||
|
@ -276,7 +387,7 @@ paths:
|
|||
- OAuth2:
|
||||
- admin
|
||||
tags:
|
||||
- ServerFollowing
|
||||
- Server Following
|
||||
summary: Follow a server
|
||||
responses:
|
||||
'204':
|
||||
|
@ -701,6 +812,85 @@ paths:
|
|||
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:
|
||||
post:
|
||||
summary: Upload a video file with its metadata
|
||||
|
@ -771,7 +961,6 @@ paths:
|
|||
- videofile
|
||||
- channelId
|
||||
- name
|
||||
- privacy
|
||||
x-code-samples:
|
||||
- lang: Shell
|
||||
source: |
|
||||
|
@ -781,7 +970,6 @@ paths:
|
|||
PASSWORD="<your_password>"
|
||||
FILE_PATH="<your_file_path>"
|
||||
CHANNEL_ID="<your_channel_id>"
|
||||
PRIVACY="1" # public: 1, unlisted: 2, private: 3
|
||||
NAME="<video_name>"
|
||||
|
||||
API_PATH="https://peertube2.cpy.re/api/v1"
|
||||
|
@ -798,7 +986,6 @@ paths:
|
|||
videofile@$FILE_PATH \
|
||||
channelId=$CHANNEL_ID \
|
||||
name=$NAME \
|
||||
privacy=$PRIVACY \
|
||||
"Authorization:Bearer $token"
|
||||
/videos/abuse:
|
||||
get:
|
||||
|
@ -806,7 +993,7 @@ paths:
|
|||
security:
|
||||
- OAuth2: []
|
||||
tags:
|
||||
- VideoAbuse
|
||||
- Video Abuse
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/start'
|
||||
- $ref: '#/components/parameters/count'
|
||||
|
@ -826,7 +1013,7 @@ paths:
|
|||
security:
|
||||
- OAuth2: []
|
||||
tags:
|
||||
- VideoAbuse
|
||||
- Video Abuse
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/id2'
|
||||
responses:
|
||||
|
@ -840,7 +1027,7 @@ paths:
|
|||
- admin
|
||||
- moderator
|
||||
tags:
|
||||
- VideoBlacklist
|
||||
- Video Blacklist
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/id2'
|
||||
responses:
|
||||
|
@ -853,7 +1040,7 @@ paths:
|
|||
- admin
|
||||
- moderator
|
||||
tags:
|
||||
- VideoBlacklist
|
||||
- Video Blacklist
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/id2'
|
||||
responses:
|
||||
|
@ -867,7 +1054,7 @@ paths:
|
|||
- admin
|
||||
- moderator
|
||||
tags:
|
||||
- VideoBlacklist
|
||||
- Video Blacklist
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/start'
|
||||
- $ref: '#/components/parameters/count'
|
||||
|
@ -885,7 +1072,7 @@ paths:
|
|||
get:
|
||||
summary: Get list of video channels
|
||||
tags:
|
||||
- VideoChannel
|
||||
- Video Channel
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/start'
|
||||
- $ref: '#/components/parameters/count'
|
||||
|
@ -904,7 +1091,7 @@ paths:
|
|||
security:
|
||||
- OAuth2: []
|
||||
tags:
|
||||
- VideoChannel
|
||||
- Video Channel
|
||||
responses:
|
||||
'204':
|
||||
$ref: '#/paths/~1users~1me/put/responses/204'
|
||||
|
@ -914,7 +1101,7 @@ paths:
|
|||
get:
|
||||
summary: Get a video channel by its id
|
||||
tags:
|
||||
- VideoChannel
|
||||
- Video Channel
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/id3'
|
||||
responses:
|
||||
|
@ -929,7 +1116,7 @@ paths:
|
|||
security:
|
||||
- OAuth2: []
|
||||
tags:
|
||||
- VideoChannel
|
||||
- Video Channel
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/id3'
|
||||
responses:
|
||||
|
@ -942,7 +1129,7 @@ paths:
|
|||
security:
|
||||
- OAuth2: []
|
||||
tags:
|
||||
- VideoChannel
|
||||
- Video Channel
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/id3'
|
||||
responses:
|
||||
|
@ -952,7 +1139,7 @@ paths:
|
|||
get:
|
||||
summary: Get videos of a video channel by its id
|
||||
tags:
|
||||
- VideoChannel
|
||||
- Video Channel
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/id3'
|
||||
responses:
|
||||
|
@ -966,7 +1153,7 @@ paths:
|
|||
get:
|
||||
summary: Get video channels of an account by its name
|
||||
tags:
|
||||
- VideoChannel
|
||||
- Video Channel
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/name'
|
||||
responses:
|
||||
|
@ -982,7 +1169,7 @@ paths:
|
|||
get:
|
||||
summary: Get the comment threads of a video by its id
|
||||
tags:
|
||||
- VideoComment
|
||||
- Video Comment
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/id2'
|
||||
- $ref: '#/components/parameters/start'
|
||||
|
@ -1000,7 +1187,7 @@ paths:
|
|||
security:
|
||||
- OAuth2: []
|
||||
tags:
|
||||
- VideoComment
|
||||
- Video Comment
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/id2'
|
||||
responses:
|
||||
|
@ -1014,7 +1201,7 @@ paths:
|
|||
get:
|
||||
summary: 'Get the comment thread by its id, of a video by its id'
|
||||
tags:
|
||||
- VideoComment
|
||||
- Video Comment
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/id2'
|
||||
- name: threadId
|
||||
|
@ -1036,7 +1223,7 @@ paths:
|
|||
security:
|
||||
- OAuth2: []
|
||||
tags:
|
||||
- VideoComment
|
||||
- Video Comment
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/id2'
|
||||
- $ref: '#/components/parameters/commentId'
|
||||
|
@ -1052,7 +1239,7 @@ paths:
|
|||
security:
|
||||
- OAuth2: []
|
||||
tags:
|
||||
- VideoComment
|
||||
- Video Comment
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/id2'
|
||||
- $ref: '#/components/parameters/commentId'
|
||||
|
@ -1065,7 +1252,7 @@ paths:
|
|||
security:
|
||||
- OAuth2: []
|
||||
tags:
|
||||
- VideoRate
|
||||
- Video Rate
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/id2'
|
||||
responses:
|
||||
|
@ -1096,8 +1283,12 @@ paths:
|
|||
items:
|
||||
$ref: '#/components/schemas/Video'
|
||||
servers:
|
||||
- url: 'https://peertube.cpy.re/api/v1'
|
||||
description: Live Test Server (live data - stable version)
|
||||
- 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:
|
||||
parameters:
|
||||
start:
|
||||
|
@ -1417,6 +1608,10 @@ components:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/VideoChannel'
|
||||
UserWatchingVideo:
|
||||
properties:
|
||||
currentTime:
|
||||
type: number
|
||||
ServerConfig:
|
||||
properties:
|
||||
signup:
|
||||
|
|
Loading…
Reference in New Issue