mirror of https://github.com/Chocobozzz/PeerTube
Add ability to cancel & delete video imports
parent
52435e467a
commit
419b520ca4
|
@ -13,7 +13,7 @@
|
||||||
<ng-template pTemplate="header">
|
<ng-template pTemplate="header">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 40px;"></th>
|
<th style="width: 40px;"></th>
|
||||||
<th style="width: 70px">Action</th>
|
<th style="width: 200px">Action</th>
|
||||||
<th style="width: 45%" i18n>Target</th>
|
<th style="width: 45%" i18n>Target</th>
|
||||||
<th style="width: 55%" i18n>Video</th>
|
<th style="width: 55%" i18n>Video</th>
|
||||||
<th style="width: 150px" i18n>State</th>
|
<th style="width: 150px" i18n>State</th>
|
||||||
|
@ -28,8 +28,9 @@
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="action-cell">
|
<td class="action-cell">
|
||||||
<my-edit-button *ngIf="isVideoImportSuccess(videoImport) && videoImport.video"
|
<my-button *ngIf="isVideoImportPending(videoImport)" i18n-label label="Cancel" icon="no" (click)="cancelImport(videoImport)"></my-button>
|
||||||
[routerLink]="getEditVideoUrl(videoImport.video)"></my-edit-button>
|
<my-delete-button *ngIf="isVideoImportFailed(videoImport) || isVideoImportCancelled(videoImport) || !videoImport.video" (click)="deleteImport(videoImport)"></my-delete-button>
|
||||||
|
<my-edit-button *ngIf="isVideoImportSuccess(videoImport) && videoImport.video" [routerLink]="getEditVideoUrl(videoImport.video)"></my-edit-button>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -37,6 +37,8 @@ export class MyVideoImportsComponent extends RestTable implements OnInit {
|
||||||
return 'badge-banned'
|
return 'badge-banned'
|
||||||
case VideoImportState.PENDING:
|
case VideoImportState.PENDING:
|
||||||
return 'badge-yellow'
|
return 'badge-yellow'
|
||||||
|
case VideoImportState.PROCESSING:
|
||||||
|
return 'badge-blue'
|
||||||
default:
|
default:
|
||||||
return 'badge-green'
|
return 'badge-green'
|
||||||
}
|
}
|
||||||
|
@ -54,6 +56,10 @@ export class MyVideoImportsComponent extends RestTable implements OnInit {
|
||||||
return videoImport.state.id === VideoImportState.FAILED
|
return videoImport.state.id === VideoImportState.FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isVideoImportCancelled (videoImport: VideoImport) {
|
||||||
|
return videoImport.state.id === VideoImportState.CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
getVideoUrl (video: { uuid: string }) {
|
getVideoUrl (video: { uuid: string }) {
|
||||||
return Video.buildWatchUrl(video)
|
return Video.buildWatchUrl(video)
|
||||||
}
|
}
|
||||||
|
@ -62,6 +68,24 @@ export class MyVideoImportsComponent extends RestTable implements OnInit {
|
||||||
return Video.buildUpdateUrl(video)
|
return Video.buildUpdateUrl(video)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteImport (videoImport: VideoImport) {
|
||||||
|
this.videoImportService.deleteVideoImport(videoImport)
|
||||||
|
.subscribe({
|
||||||
|
next: () => this.reloadData(),
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelImport (videoImport: VideoImport) {
|
||||||
|
this.videoImportService.cancelVideoImport(videoImport)
|
||||||
|
.subscribe({
|
||||||
|
next: () => this.reloadData(),
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
protected reloadData () {
|
protected reloadData () {
|
||||||
this.videoImportService.getMyVideoImports(this.pagination, this.sort)
|
this.videoImportService.getMyVideoImports(this.pagination, this.sort)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<span class="action-button" [ngClass]="getClasses()" [ngbTooltip]="getTitle()" tabindex="0">
|
<span class="action-button" [ngClass]="getClasses()" [ngbTooltip]="title" tabindex="0">
|
||||||
<my-global-icon *ngIf="!loading" [iconName]="icon"></my-global-icon>
|
<my-global-icon *ngIf="icon && !loading" [iconName]="icon"></my-global-icon>
|
||||||
<my-small-loader [loading]="loading"></my-small-loader>
|
<my-small-loader [loading]="loading"></my-small-loader>
|
||||||
|
|
||||||
<span *ngIf="label" class="button-label">{{ label }}</span>
|
<span *ngIf="label" class="button-label">{{ label }}</span>
|
||||||
|
|
|
@ -16,10 +16,6 @@ export class ButtonComponent {
|
||||||
@Input() disabled = false
|
@Input() disabled = false
|
||||||
@Input() responsiveLabel = false
|
@Input() responsiveLabel = false
|
||||||
|
|
||||||
getTitle () {
|
|
||||||
return this.title || this.label
|
|
||||||
}
|
|
||||||
|
|
||||||
getClasses () {
|
getClasses () {
|
||||||
return {
|
return {
|
||||||
[this.className]: true,
|
[this.className]: true,
|
||||||
|
|
|
@ -20,10 +20,6 @@ export class DeleteButtonComponent implements OnInit {
|
||||||
// <my-delete-button label /> Use default label
|
// <my-delete-button label /> Use default label
|
||||||
if (this.label === '') {
|
if (this.label === '') {
|
||||||
this.label = $localize`Delete`
|
this.label = $localize`Delete`
|
||||||
|
|
||||||
if (!this.title) {
|
|
||||||
this.title = this.label
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,16 @@ export class VideoImportService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteVideoImport (videoImport: VideoImport) {
|
||||||
|
return this.authHttp.delete(VideoImportService.BASE_VIDEO_IMPORT_URL + videoImport.id)
|
||||||
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelVideoImport (videoImport: VideoImport) {
|
||||||
|
return this.authHttp.post(VideoImportService.BASE_VIDEO_IMPORT_URL + videoImport.id + '/cancel', {})
|
||||||
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
|
}
|
||||||
|
|
||||||
private buildImportVideoObject (video: VideoUpdate): VideoImportCreate {
|
private buildImportVideoObject (video: VideoUpdate): VideoImportCreate {
|
||||||
const language = video.language || null
|
const language = video.language || null
|
||||||
const licence = video.licence || null
|
const licence = video.licence || null
|
||||||
|
|
|
@ -257,7 +257,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin peertube-button {
|
@mixin peertube-button {
|
||||||
@include padding(0, 17px, 0, 13px);
|
padding: 0 13px;
|
||||||
|
|
||||||
border: 0;
|
border: 0;
|
||||||
font-weight: $font-semibold;
|
font-weight: $font-semibold;
|
||||||
|
@ -270,6 +270,10 @@
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
my-global-icon + * {
|
||||||
|
@include margin-right(4px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin peertube-button-link {
|
@mixin peertube-button-link {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { Job, JobState, JobType, ResultList, UserRight } from '@shared/models'
|
import { HttpStatusCode, Job, JobState, JobType, ResultList, UserRight } from '@shared/models'
|
||||||
import { isArray } from '../../helpers/custom-validators/misc'
|
import { isArray } from '../../helpers/custom-validators/misc'
|
||||||
import { JobQueue } from '../../lib/job-queue'
|
import { JobQueue } from '../../lib/job-queue'
|
||||||
import {
|
import {
|
||||||
|
@ -16,6 +16,18 @@ import { listJobsValidator } from '../../middlewares/validators/jobs'
|
||||||
|
|
||||||
const jobsRouter = express.Router()
|
const jobsRouter = express.Router()
|
||||||
|
|
||||||
|
jobsRouter.post('/pause',
|
||||||
|
authenticate,
|
||||||
|
ensureUserHasRight(UserRight.MANAGE_JOBS),
|
||||||
|
asyncMiddleware(pauseJobQueue)
|
||||||
|
)
|
||||||
|
|
||||||
|
jobsRouter.post('/resume',
|
||||||
|
authenticate,
|
||||||
|
ensureUserHasRight(UserRight.MANAGE_JOBS),
|
||||||
|
asyncMiddleware(resumeJobQueue)
|
||||||
|
)
|
||||||
|
|
||||||
jobsRouter.get('/:state?',
|
jobsRouter.get('/:state?',
|
||||||
openapiOperationDoc({ operationId: 'getJobs' }),
|
openapiOperationDoc({ operationId: 'getJobs' }),
|
||||||
authenticate,
|
authenticate,
|
||||||
|
@ -36,6 +48,18 @@ export {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function pauseJobQueue (req: express.Request, res: express.Response) {
|
||||||
|
await JobQueue.Instance.pause()
|
||||||
|
|
||||||
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resumeJobQueue (req: express.Request, res: express.Response) {
|
||||||
|
await JobQueue.Instance.resume()
|
||||||
|
|
||||||
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||||
|
}
|
||||||
|
|
||||||
async function listJobs (req: express.Request, res: express.Response) {
|
async function listJobs (req: express.Request, res: express.Response) {
|
||||||
const state = req.params.state as JobState
|
const state = req.params.state as JobState
|
||||||
const asc = req.query.sort === 'createdAt'
|
const asc = req.query.sort === 'createdAt'
|
||||||
|
|
|
@ -19,7 +19,15 @@ import {
|
||||||
MVideoWithBlacklistLight
|
MVideoWithBlacklistLight
|
||||||
} from '@server/types/models'
|
} from '@server/types/models'
|
||||||
import { MVideoImportFormattable } from '@server/types/models/video/video-import'
|
import { MVideoImportFormattable } from '@server/types/models/video/video-import'
|
||||||
import { ServerErrorCode, ThumbnailType, VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '@shared/models'
|
import {
|
||||||
|
HttpStatusCode,
|
||||||
|
ServerErrorCode,
|
||||||
|
ThumbnailType,
|
||||||
|
VideoImportCreate,
|
||||||
|
VideoImportState,
|
||||||
|
VideoPrivacy,
|
||||||
|
VideoState
|
||||||
|
} from '@shared/models'
|
||||||
import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
|
import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
|
||||||
import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
|
import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
|
||||||
import { isArray } from '../../../helpers/custom-validators/misc'
|
import { isArray } from '../../../helpers/custom-validators/misc'
|
||||||
|
@ -34,7 +42,14 @@ import { getLocalVideoActivityPubUrl } from '../../../lib/activitypub/url'
|
||||||
import { JobQueue } from '../../../lib/job-queue/job-queue'
|
import { JobQueue } from '../../../lib/job-queue/job-queue'
|
||||||
import { updateVideoMiniatureFromExisting, updateVideoMiniatureFromUrl } from '../../../lib/thumbnail'
|
import { updateVideoMiniatureFromExisting, updateVideoMiniatureFromUrl } from '../../../lib/thumbnail'
|
||||||
import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
|
import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
|
||||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares'
|
import {
|
||||||
|
asyncMiddleware,
|
||||||
|
asyncRetryTransactionMiddleware,
|
||||||
|
authenticate,
|
||||||
|
videoImportAddValidator,
|
||||||
|
videoImportCancelValidator,
|
||||||
|
videoImportDeleteValidator
|
||||||
|
} from '../../../middlewares'
|
||||||
import { VideoModel } from '../../../models/video/video'
|
import { VideoModel } from '../../../models/video/video'
|
||||||
import { VideoCaptionModel } from '../../../models/video/video-caption'
|
import { VideoCaptionModel } from '../../../models/video/video-caption'
|
||||||
import { VideoImportModel } from '../../../models/video/video-import'
|
import { VideoImportModel } from '../../../models/video/video-import'
|
||||||
|
@ -59,6 +74,18 @@ videoImportsRouter.post('/imports',
|
||||||
asyncRetryTransactionMiddleware(addVideoImport)
|
asyncRetryTransactionMiddleware(addVideoImport)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
videoImportsRouter.post('/imports/:id/cancel',
|
||||||
|
authenticate,
|
||||||
|
asyncMiddleware(videoImportCancelValidator),
|
||||||
|
asyncRetryTransactionMiddleware(cancelVideoImport)
|
||||||
|
)
|
||||||
|
|
||||||
|
videoImportsRouter.delete('/imports/:id',
|
||||||
|
authenticate,
|
||||||
|
asyncMiddleware(videoImportDeleteValidator),
|
||||||
|
asyncRetryTransactionMiddleware(deleteVideoImport)
|
||||||
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -67,6 +94,23 @@ export {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function deleteVideoImport (req: express.Request, res: express.Response) {
|
||||||
|
const videoImport = res.locals.videoImport
|
||||||
|
|
||||||
|
await videoImport.destroy()
|
||||||
|
|
||||||
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelVideoImport (req: express.Request, res: express.Response) {
|
||||||
|
const videoImport = res.locals.videoImport
|
||||||
|
|
||||||
|
videoImport.state = VideoImportState.CANCELLED
|
||||||
|
await videoImport.save()
|
||||||
|
|
||||||
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||||
|
}
|
||||||
|
|
||||||
function addVideoImport (req: express.Request, res: express.Response) {
|
function addVideoImport (req: express.Request, res: express.Response) {
|
||||||
if (req.body.targetUrl) return addYoutubeDLImport(req, res)
|
if (req.body.targetUrl) return addYoutubeDLImport(req, res)
|
||||||
|
|
||||||
|
|
|
@ -441,7 +441,9 @@ const VIDEO_IMPORT_STATES: { [ id in VideoImportState ]: string } = {
|
||||||
[VideoImportState.FAILED]: 'Failed',
|
[VideoImportState.FAILED]: 'Failed',
|
||||||
[VideoImportState.PENDING]: 'Pending',
|
[VideoImportState.PENDING]: 'Pending',
|
||||||
[VideoImportState.SUCCESS]: 'Success',
|
[VideoImportState.SUCCESS]: 'Success',
|
||||||
[VideoImportState.REJECTED]: 'Rejected'
|
[VideoImportState.REJECTED]: 'Rejected',
|
||||||
|
[VideoImportState.CANCELLED]: 'Cancelled',
|
||||||
|
[VideoImportState.PROCESSING]: 'Processing'
|
||||||
}
|
}
|
||||||
|
|
||||||
const ABUSE_STATES: { [ id in AbuseState ]: string } = {
|
const ABUSE_STATES: { [ id in AbuseState ]: string } = {
|
||||||
|
|
|
@ -42,8 +42,17 @@ import { generateVideoMiniature } from '../../thumbnail'
|
||||||
async function processVideoImport (job: Job) {
|
async function processVideoImport (job: Job) {
|
||||||
const payload = job.data as VideoImportPayload
|
const payload = job.data as VideoImportPayload
|
||||||
|
|
||||||
if (payload.type === 'youtube-dl') return processYoutubeDLImport(job, payload)
|
const videoImport = await getVideoImportOrDie(payload.videoImportId)
|
||||||
if (payload.type === 'magnet-uri' || payload.type === 'torrent-file') return processTorrentImport(job, payload)
|
if (videoImport.state === VideoImportState.CANCELLED) {
|
||||||
|
logger.info('Do not process import since it has been cancelled', { payload })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
videoImport.state = VideoImportState.PROCESSING
|
||||||
|
await videoImport.save()
|
||||||
|
|
||||||
|
if (payload.type === 'youtube-dl') return processYoutubeDLImport(job, videoImport, payload)
|
||||||
|
if (payload.type === 'magnet-uri' || payload.type === 'torrent-file') return processTorrentImport(job, videoImport, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
@ -54,15 +63,11 @@ export {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function processTorrentImport (job: Job, payload: VideoImportTorrentPayload) {
|
async function processTorrentImport (job: Job, videoImport: MVideoImportDefault, payload: VideoImportTorrentPayload) {
|
||||||
logger.info('Processing torrent video import in job %d.', job.id)
|
logger.info('Processing torrent video import in job %d.', job.id)
|
||||||
|
|
||||||
const videoImport = await getVideoImportOrDie(payload.videoImportId)
|
const options = { type: payload.type, videoImportId: payload.videoImportId }
|
||||||
|
|
||||||
const options = {
|
|
||||||
type: payload.type,
|
|
||||||
videoImportId: payload.videoImportId
|
|
||||||
}
|
|
||||||
const target = {
|
const target = {
|
||||||
torrentName: videoImport.torrentName ? getSecureTorrentName(videoImport.torrentName) : undefined,
|
torrentName: videoImport.torrentName ? getSecureTorrentName(videoImport.torrentName) : undefined,
|
||||||
uri: videoImport.magnetUri
|
uri: videoImport.magnetUri
|
||||||
|
@ -70,14 +75,10 @@ async function processTorrentImport (job: Job, payload: VideoImportTorrentPayloa
|
||||||
return processFile(() => downloadWebTorrentVideo(target, VIDEO_IMPORT_TIMEOUT), videoImport, options)
|
return processFile(() => downloadWebTorrentVideo(target, VIDEO_IMPORT_TIMEOUT), videoImport, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processYoutubeDLImport (job: Job, payload: VideoImportYoutubeDLPayload) {
|
async function processYoutubeDLImport (job: Job, videoImport: MVideoImportDefault, payload: VideoImportYoutubeDLPayload) {
|
||||||
logger.info('Processing youtubeDL video import in job %d.', job.id)
|
logger.info('Processing youtubeDL video import in job %d.', job.id)
|
||||||
|
|
||||||
const videoImport = await getVideoImportOrDie(payload.videoImportId)
|
const options = { type: payload.type, videoImportId: videoImport.id }
|
||||||
const options = {
|
|
||||||
type: payload.type,
|
|
||||||
videoImportId: videoImport.id
|
|
||||||
}
|
|
||||||
|
|
||||||
const youtubeDL = new YoutubeDLWrapper(videoImport.targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod'))
|
const youtubeDL = new YoutubeDLWrapper(videoImport.targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod'))
|
||||||
|
|
||||||
|
|
|
@ -162,6 +162,18 @@ class JobQueue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async pause () {
|
||||||
|
for (const handler of Object.keys(this.queues)) {
|
||||||
|
await this.queues[handler].pause(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async resume () {
|
||||||
|
for (const handler of Object.keys(this.queues)) {
|
||||||
|
await this.queues[handler].resume(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createJob (obj: CreateJobArgument, options: CreateJobOptions = {}): void {
|
createJob (obj: CreateJobArgument, options: CreateJobOptions = {}): void {
|
||||||
this.createJobWithPromise(obj, options)
|
this.createJobWithPromise(obj, options)
|
||||||
.catch(err => logger.error('Cannot create job.', { err, obj }))
|
.catch(err => logger.error('Cannot create job.', { err, obj }))
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { body } from 'express-validator'
|
import { body, param } from 'express-validator'
|
||||||
|
import { isValid as isIPValid, parse as parseIP } from 'ipaddr.js'
|
||||||
import { isPreImportVideoAccepted } from '@server/lib/moderation'
|
import { isPreImportVideoAccepted } from '@server/lib/moderation'
|
||||||
import { Hooks } from '@server/lib/plugins/hooks'
|
import { Hooks } from '@server/lib/plugins/hooks'
|
||||||
import { HttpStatusCode } from '@shared/models'
|
import { MUserAccountId, MVideoImport } from '@server/types/models'
|
||||||
|
import { HttpStatusCode, UserRight, VideoImportState } from '@shared/models'
|
||||||
import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model'
|
import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model'
|
||||||
import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc'
|
import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc'
|
||||||
import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports'
|
import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports'
|
||||||
|
@ -11,9 +13,8 @@ import { cleanUpReqFiles } from '../../../helpers/express-utils'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { CONFIG } from '../../../initializers/config'
|
import { CONFIG } from '../../../initializers/config'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
|
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
|
||||||
import { areValidationErrors, doesVideoChannelOfAccountExist } from '../shared'
|
import { areValidationErrors, doesVideoChannelOfAccountExist, doesVideoImportExist } from '../shared'
|
||||||
import { getCommonVideoEditAttributes } from './videos'
|
import { getCommonVideoEditAttributes } from './videos'
|
||||||
import { isValid as isIPValid, parse as parseIP } from 'ipaddr.js'
|
|
||||||
|
|
||||||
const videoImportAddValidator = getCommonVideoEditAttributes().concat([
|
const videoImportAddValidator = getCommonVideoEditAttributes().concat([
|
||||||
body('channelId')
|
body('channelId')
|
||||||
|
@ -95,10 +96,58 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const videoImportDeleteValidator = [
|
||||||
|
param('id')
|
||||||
|
.custom(isIdValid).withMessage('Should have correct import id'),
|
||||||
|
|
||||||
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking videoImportDeleteValidator parameters', { parameters: req.params })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
if (!await doesVideoImportExist(parseInt(req.params.id), res)) return
|
||||||
|
if (!checkUserCanManageImport(res.locals.oauth.token.user, res.locals.videoImport, res)) return
|
||||||
|
|
||||||
|
if (res.locals.videoImport.state === VideoImportState.PENDING) {
|
||||||
|
return res.fail({
|
||||||
|
status: HttpStatusCode.CONFLICT_409,
|
||||||
|
message: 'Cannot delete a pending video import. Cancel it or wait for the end of the import first.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const videoImportCancelValidator = [
|
||||||
|
param('id')
|
||||||
|
.custom(isIdValid).withMessage('Should have correct import id'),
|
||||||
|
|
||||||
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking videoImportCancelValidator parameters', { parameters: req.params })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
if (!await doesVideoImportExist(parseInt(req.params.id), res)) return
|
||||||
|
if (!checkUserCanManageImport(res.locals.oauth.token.user, res.locals.videoImport, res)) return
|
||||||
|
|
||||||
|
if (res.locals.videoImport.state !== VideoImportState.PENDING) {
|
||||||
|
return res.fail({
|
||||||
|
status: HttpStatusCode.CONFLICT_409,
|
||||||
|
message: 'Cannot cancel a non pending video import.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
videoImportAddValidator
|
videoImportAddValidator,
|
||||||
|
videoImportCancelValidator,
|
||||||
|
videoImportDeleteValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
@ -132,3 +181,15 @@ async function isImportAccepted (req: express.Request, res: express.Response) {
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkUserCanManageImport (user: MUserAccountId, videoImport: MVideoImport, res: express.Response) {
|
||||||
|
if (user.hasRight(UserRight.MANAGE_VIDEO_IMPORTS) === false && videoImport.userId !== user.id) {
|
||||||
|
res.fail({
|
||||||
|
status: HttpStatusCode.FORBIDDEN_403,
|
||||||
|
message: 'Cannot manage video import of another user'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,14 @@
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared'
|
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared'
|
||||||
import { HttpStatusCode } from '@shared/models'
|
import { HttpStatusCode } from '@shared/models'
|
||||||
import { cleanupTests, createSingleServer, makeGetRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
|
import {
|
||||||
|
cleanupTests,
|
||||||
|
createSingleServer,
|
||||||
|
makeGetRequest,
|
||||||
|
makePostBodyRequest,
|
||||||
|
PeerTubeServer,
|
||||||
|
setAccessTokensToServers
|
||||||
|
} from '@shared/server-commands'
|
||||||
|
|
||||||
describe('Test jobs API validators', function () {
|
describe('Test jobs API validators', function () {
|
||||||
const path = '/api/v1/jobs/failed'
|
const path = '/api/v1/jobs/failed'
|
||||||
|
@ -76,7 +83,41 @@ describe('Test jobs API validators', function () {
|
||||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When pausing/resuming the job queue', async function () {
|
||||||
|
const commands = [ 'pause', 'resume' ]
|
||||||
|
|
||||||
|
it('Should fail with a non authenticated user', async function () {
|
||||||
|
for (const command of commands) {
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
path: '/api/v1/jobs/' + command,
|
||||||
|
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a non admin user', async function () {
|
||||||
|
for (const command of commands) {
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
path: '/api/v1/jobs/' + command,
|
||||||
|
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct params', async function () {
|
||||||
|
for (const command of commands) {
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
path: '/api/v1/jobs/' + command,
|
||||||
|
token: server.accessToken,
|
||||||
|
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
|
|
|
@ -12,7 +12,9 @@ import {
|
||||||
makePostBodyRequest,
|
makePostBodyRequest,
|
||||||
makeUploadRequest,
|
makeUploadRequest,
|
||||||
PeerTubeServer,
|
PeerTubeServer,
|
||||||
setAccessTokensToServers
|
setAccessTokensToServers,
|
||||||
|
setDefaultVideoChannel,
|
||||||
|
waitJobs
|
||||||
} from '@shared/server-commands'
|
} from '@shared/server-commands'
|
||||||
|
|
||||||
describe('Test video imports API validator', function () {
|
describe('Test video imports API validator', function () {
|
||||||
|
@ -29,6 +31,7 @@ describe('Test video imports API validator', function () {
|
||||||
server = await createSingleServer(1)
|
server = await createSingleServer(1)
|
||||||
|
|
||||||
await setAccessTokensToServers([ server ])
|
await setAccessTokensToServers([ server ])
|
||||||
|
await setDefaultVideoChannel([ server ])
|
||||||
|
|
||||||
const username = 'user1'
|
const username = 'user1'
|
||||||
const password = 'my super password'
|
const password = 'my super password'
|
||||||
|
@ -347,6 +350,67 @@ describe('Test video imports API validator', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Deleting/cancelling a video import', function () {
|
||||||
|
let importId: number
|
||||||
|
|
||||||
|
async function importVideo () {
|
||||||
|
const attributes = { channelId: server.store.channel.id, targetUrl: FIXTURE_URLS.goodVideo }
|
||||||
|
const res = await server.imports.importVideo({ attributes })
|
||||||
|
|
||||||
|
return res.id
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
importId = await importVideo()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an invalid import id', async function () {
|
||||||
|
await server.imports.cancel({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||||
|
await server.imports.delete({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an unknown import id', async function () {
|
||||||
|
await server.imports.cancel({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||||
|
await server.imports.delete({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail without token', async function () {
|
||||||
|
await server.imports.cancel({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||||
|
await server.imports.delete({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with another user token', async function () {
|
||||||
|
await server.imports.cancel({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||||
|
await server.imports.delete({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail to cancel non pending import', async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
await server.imports.cancel({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed to delete an import', async function () {
|
||||||
|
await server.imports.delete({ importId })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail to delete a pending import', async function () {
|
||||||
|
await server.jobs.pauseJobQueue()
|
||||||
|
|
||||||
|
importId = await importVideo()
|
||||||
|
|
||||||
|
await server.imports.delete({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed to cancel an import', async function () {
|
||||||
|
importId = await importVideo()
|
||||||
|
|
||||||
|
await server.imports.cancel({ importId })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await cleanupTests([ server ])
|
await cleanupTests([ server ])
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
setAccessTokensToServers,
|
setAccessTokensToServers,
|
||||||
waitJobs
|
waitJobs
|
||||||
} from '@shared/server-commands'
|
} from '@shared/server-commands'
|
||||||
|
import { wait } from '@shared/core-utils'
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
|
@ -91,6 +92,30 @@ describe('Test jobs', function () {
|
||||||
expect(jobs.find(j => j.state === 'completed')).to.not.be.undefined
|
expect(jobs.find(j => j.state === 'completed')).to.not.be.undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should pause the job queue', async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
|
||||||
|
await servers[1].jobs.pauseJobQueue()
|
||||||
|
|
||||||
|
await servers[1].videos.upload({ attributes: { name: 'video2' } })
|
||||||
|
|
||||||
|
await wait(5000)
|
||||||
|
|
||||||
|
const body = await servers[1].jobs.list({ state: 'waiting', jobType: 'video-transcoding' })
|
||||||
|
expect(body.data).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should resume the job queue', async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
|
||||||
|
await servers[1].jobs.resumeJobQueue()
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
const body = await servers[1].jobs.list({ state: 'waiting', jobType: 'video-transcoding' })
|
||||||
|
expect(body.data).to.have.lengthOf(0)
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await cleanupTests(servers)
|
await cleanupTests(servers)
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { pathExists, readdir, remove } from 'fs-extra'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { FIXTURE_URLS, testCaptionFile, testImage } from '@server/tests/shared'
|
import { FIXTURE_URLS, testCaptionFile, testImage } from '@server/tests/shared'
|
||||||
import { areHttpImportTestsDisabled } from '@shared/core-utils'
|
import { areHttpImportTestsDisabled } from '@shared/core-utils'
|
||||||
import { VideoPrivacy, VideoResolution } from '@shared/models'
|
import { HttpStatusCode, Video, VideoImportState, VideoPrivacy, VideoResolution, VideoState } from '@shared/models'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createMultipleServers,
|
createMultipleServers,
|
||||||
|
@ -382,6 +382,85 @@ describe('Test video imports', function () {
|
||||||
|
|
||||||
runSuite('yt-dlp')
|
runSuite('yt-dlp')
|
||||||
|
|
||||||
|
describe('Delete/cancel an import', function () {
|
||||||
|
let server: PeerTubeServer
|
||||||
|
|
||||||
|
let finishedImportId: number
|
||||||
|
let finishedVideo: Video
|
||||||
|
let pendingImportId: number
|
||||||
|
|
||||||
|
async function importVideo (name: string) {
|
||||||
|
const attributes = { name, channelId: server.store.channel.id, targetUrl: FIXTURE_URLS.goodVideo }
|
||||||
|
const res = await server.imports.importVideo({ attributes })
|
||||||
|
|
||||||
|
return res.id
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(120_000)
|
||||||
|
|
||||||
|
server = await createSingleServer(1)
|
||||||
|
|
||||||
|
await setAccessTokensToServers([ server ])
|
||||||
|
await setDefaultVideoChannel([ server ])
|
||||||
|
|
||||||
|
finishedImportId = await importVideo('finished')
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
await server.jobs.pauseJobQueue()
|
||||||
|
pendingImportId = await importVideo('pending')
|
||||||
|
|
||||||
|
const { data } = await server.imports.getMyVideoImports()
|
||||||
|
expect(data).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
finishedVideo = data.find(i => i.id === finishedImportId).video
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should delete a video import', async function () {
|
||||||
|
await server.imports.delete({ importId: finishedImportId })
|
||||||
|
|
||||||
|
const { data } = await server.imports.getMyVideoImports()
|
||||||
|
expect(data).to.have.lengthOf(1)
|
||||||
|
expect(data[0].id).to.equal(pendingImportId)
|
||||||
|
expect(data[0].state.id).to.equal(VideoImportState.PENDING)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not have deleted the associated video', async function () {
|
||||||
|
const video = await server.videos.get({ id: finishedVideo.id, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
expect(video.name).to.equal('finished')
|
||||||
|
expect(video.state.id).to.equal(VideoState.PUBLISHED)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should cancel a video import', async function () {
|
||||||
|
await server.imports.cancel({ importId: pendingImportId })
|
||||||
|
|
||||||
|
const { data } = await server.imports.getMyVideoImports()
|
||||||
|
expect(data).to.have.lengthOf(1)
|
||||||
|
expect(data[0].id).to.equal(pendingImportId)
|
||||||
|
expect(data[0].state.id).to.equal(VideoImportState.CANCELLED)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not have processed the cancelled video import', async function () {
|
||||||
|
this.timeout(60_000)
|
||||||
|
|
||||||
|
await server.jobs.resumeJobQueue()
|
||||||
|
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
const { data } = await server.imports.getMyVideoImports()
|
||||||
|
expect(data).to.have.lengthOf(1)
|
||||||
|
expect(data[0].id).to.equal(pendingImportId)
|
||||||
|
expect(data[0].state.id).to.equal(VideoImportState.CANCELLED)
|
||||||
|
expect(data[0].video.state.id).to.equal(VideoState.TO_IMPORT)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should delete the cancelled video import', async function () {
|
||||||
|
await server.imports.delete({ importId: pendingImportId })
|
||||||
|
const { data } = await server.imports.getMyVideoImports()
|
||||||
|
expect(data).to.have.lengthOf(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('Auto update', function () {
|
describe('Auto update', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
|
||||||
|
|
|
@ -41,5 +41,7 @@ export const enum UserRight {
|
||||||
MANAGE_VIDEOS_REDUNDANCIES,
|
MANAGE_VIDEOS_REDUNDANCIES,
|
||||||
|
|
||||||
MANAGE_VIDEO_FILES,
|
MANAGE_VIDEO_FILES,
|
||||||
RUN_VIDEO_TRANSCODING
|
RUN_VIDEO_TRANSCODING,
|
||||||
|
|
||||||
|
MANAGE_VIDEO_IMPORTS
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,5 +2,7 @@ export const enum VideoImportState {
|
||||||
PENDING = 1,
|
PENDING = 1,
|
||||||
SUCCESS = 2,
|
SUCCESS = 2,
|
||||||
FAILED = 3,
|
FAILED = 3,
|
||||||
REJECTED = 4
|
REJECTED = 4,
|
||||||
|
CANCELLED = 5,
|
||||||
|
PROCESSING = 6
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,30 @@ export class JobsCommand extends AbstractCommand {
|
||||||
return data[0]
|
return data[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pauseJobQueue (options: OverrideCommandOptions = {}) {
|
||||||
|
const path = '/api/v1/jobs/pause'
|
||||||
|
|
||||||
|
return this.postBodyRequest({
|
||||||
|
...options,
|
||||||
|
|
||||||
|
path,
|
||||||
|
implicitToken: true,
|
||||||
|
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeJobQueue (options: OverrideCommandOptions = {}) {
|
||||||
|
const path = '/api/v1/jobs/resume'
|
||||||
|
|
||||||
|
return this.postBodyRequest({
|
||||||
|
...options,
|
||||||
|
|
||||||
|
path,
|
||||||
|
implicitToken: true,
|
||||||
|
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
list (options: OverrideCommandOptions & {
|
list (options: OverrideCommandOptions & {
|
||||||
state?: JobState
|
state?: JobState
|
||||||
jobType?: JobType
|
jobType?: JobType
|
||||||
|
|
|
@ -26,6 +26,34 @@ export class ImportsCommand extends AbstractCommand {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete (options: OverrideCommandOptions & {
|
||||||
|
importId: number
|
||||||
|
}) {
|
||||||
|
const path = '/api/v1/videos/imports/' + options.importId
|
||||||
|
|
||||||
|
return this.deleteRequest({
|
||||||
|
...options,
|
||||||
|
|
||||||
|
path,
|
||||||
|
implicitToken: true,
|
||||||
|
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel (options: OverrideCommandOptions & {
|
||||||
|
importId: number
|
||||||
|
}) {
|
||||||
|
const path = '/api/v1/videos/imports/' + options.importId + '/cancel'
|
||||||
|
|
||||||
|
return this.postBodyRequest({
|
||||||
|
...options,
|
||||||
|
|
||||||
|
path,
|
||||||
|
implicitToken: true,
|
||||||
|
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
getMyVideoImports (options: OverrideCommandOptions & {
|
getMyVideoImports (options: OverrideCommandOptions & {
|
||||||
sort?: string
|
sort?: string
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
|
|
@ -252,6 +252,8 @@ tags:
|
||||||
|
|
||||||
The import function is practical when the desired video/audio is available online. It makes PeerTube
|
The import function is practical when the desired video/audio is available online. It makes PeerTube
|
||||||
download it for you, saving you as much bandwidth and avoiding any instability or limitation your network might have.
|
download it for you, saving you as much bandwidth and avoiding any instability or limitation your network might have.
|
||||||
|
- name: Video Imports
|
||||||
|
description: Operations dealing with listing, adding and removing video imports.
|
||||||
- name: Video Captions
|
- name: Video Captions
|
||||||
description: Operations dealing with listing, adding and removing closed captions of a video.
|
description: Operations dealing with listing, adding and removing closed captions of a video.
|
||||||
- name: Video Channels
|
- name: Video Channels
|
||||||
|
@ -306,6 +308,7 @@ x-tagGroups:
|
||||||
tags:
|
tags:
|
||||||
- Video
|
- Video
|
||||||
- Video Upload
|
- Video Upload
|
||||||
|
- Video Imports
|
||||||
- Video Captions
|
- Video Captions
|
||||||
- Video Channels
|
- Video Channels
|
||||||
- Video Comments
|
- Video Comments
|
||||||
|
@ -587,6 +590,30 @@ paths:
|
||||||
'204':
|
'204':
|
||||||
description: successful operation
|
description: successful operation
|
||||||
|
|
||||||
|
/jobs/pause:
|
||||||
|
post:
|
||||||
|
summary: Pause job queue
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- admin
|
||||||
|
tags:
|
||||||
|
- Job
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: successful operation
|
||||||
|
|
||||||
|
/jobs/resume:
|
||||||
|
post:
|
||||||
|
summary: Resume job queue
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- admin
|
||||||
|
tags:
|
||||||
|
- Job
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: successful operation
|
||||||
|
|
||||||
/jobs/{state}:
|
/jobs/{state}:
|
||||||
get:
|
get:
|
||||||
summary: List instance jobs
|
summary: List instance jobs
|
||||||
|
@ -2166,7 +2193,7 @@ paths:
|
||||||
security:
|
security:
|
||||||
- OAuth2: []
|
- OAuth2: []
|
||||||
tags:
|
tags:
|
||||||
- Video
|
- Video Imports
|
||||||
- Video Upload
|
- Video Upload
|
||||||
requestBody:
|
requestBody:
|
||||||
content:
|
content:
|
||||||
|
@ -2194,6 +2221,34 @@ paths:
|
||||||
'409':
|
'409':
|
||||||
description: HTTP or Torrent/magnetURI import not enabled
|
description: HTTP or Torrent/magnetURI import not enabled
|
||||||
|
|
||||||
|
/videos/imports/{id}/cancel:
|
||||||
|
post:
|
||||||
|
summary: Cancel video import
|
||||||
|
description: Cancel a pending video import
|
||||||
|
security:
|
||||||
|
- OAuth2: []
|
||||||
|
tags:
|
||||||
|
- Video Imports
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/id'
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: successful operation
|
||||||
|
|
||||||
|
/videos/imports/{id}:
|
||||||
|
delete:
|
||||||
|
summary: Delete video import
|
||||||
|
description: Delete ended video import
|
||||||
|
security:
|
||||||
|
- OAuth2: []
|
||||||
|
tags:
|
||||||
|
- Video Imports
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/id'
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: successful operation
|
||||||
|
|
||||||
/videos/live:
|
/videos/live:
|
||||||
post:
|
post:
|
||||||
summary: Create a live
|
summary: Create a live
|
||||||
|
@ -4767,7 +4822,7 @@ components:
|
||||||
name: id
|
name: id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
description: The user id
|
description: Entity id
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/id'
|
$ref: '#/components/schemas/id'
|
||||||
idOrUUID:
|
idOrUUID:
|
||||||
|
|
Loading…
Reference in New Issue