1
0
Fork 0

Merge branch 'develop' of https://github.com/Chocobozzz/PeerTube into move-utils-to-shared

This commit is contained in:
buoyantair 2018-11-18 21:55:52 +05:30
commit b9f234371b
40 changed files with 727 additions and 131 deletions

View File

@ -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
}

View File

@ -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>

View File

@ -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

View File

@ -60,6 +60,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
hide () {
this.closingModal = true
this.openedModal.close()
this.form.reset()
}
isReplacingExistingCaption () {

View File

@ -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>

View File

@ -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;

View File

@ -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)
}
)

View File

@ -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>

View File

@ -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;

View File

@ -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)
}
)

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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)
}
)

View File

@ -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>

View File

@ -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()

View File

@ -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

View File

Before

Width:  |  Height:  |  Size: 738 B

After

Width:  |  Height:  |  Size: 738 B

View File

@ -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

View File

@ -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');
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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',

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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.' })

View File

@ -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
}
}

View File

@ -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,

View File

@ -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()

View File

@ -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 ])
})
})

View File

@ -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) {

View File

@ -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)
})
})

View File

@ -1,5 +1,6 @@
import './config'
import './email'
import './follow-constraints'
import './follows'
import './handle-down'
import './jobs'

View File

@ -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: