diff options
author | Chocobozzz <me@florianbigard.com> | 2022-01-19 14:23:00 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-01-19 14:31:05 +0100 |
commit | 419b520ca4434d17f3505013174e195c3a316716 (patch) | |
tree | 24dbf663c4e11e970cb780f96e6eb3efe023b222 | |
parent | 52435e467a0b30175a10af1dd3ae10d7d564d8ae (diff) | |
download | PeerTube-419b520ca4434d17f3505013174e195c3a316716.tar.gz PeerTube-419b520ca4434d17f3505013174e195c3a316716.tar.zst PeerTube-419b520ca4434d17f3505013174e195c3a316716.zip |
Add ability to cancel & delete video imports
22 files changed, 539 insertions, 44 deletions
diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.html b/client/src/app/+my-library/my-video-imports/my-video-imports.component.html index bd29b11c8..e0d4e8f14 100644 --- a/client/src/app/+my-library/my-video-imports/my-video-imports.component.html +++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.html | |||
@@ -13,7 +13,7 @@ | |||
13 | <ng-template pTemplate="header"> | 13 | <ng-template pTemplate="header"> |
14 | <tr> | 14 | <tr> |
15 | <th style="width: 40px;"></th> | 15 | <th style="width: 40px;"></th> |
16 | <th style="width: 70px">Action</th> | 16 | <th style="width: 200px">Action</th> |
17 | <th style="width: 45%" i18n>Target</th> | 17 | <th style="width: 45%" i18n>Target</th> |
18 | <th style="width: 55%" i18n>Video</th> | 18 | <th style="width: 55%" i18n>Video</th> |
19 | <th style="width: 150px" i18n>State</th> | 19 | <th style="width: 150px" i18n>State</th> |
@@ -28,8 +28,9 @@ | |||
28 | </td> | 28 | </td> |
29 | 29 | ||
30 | <td class="action-cell"> | 30 | <td class="action-cell"> |
31 | <my-edit-button *ngIf="isVideoImportSuccess(videoImport) && videoImport.video" | 31 | <my-button *ngIf="isVideoImportPending(videoImport)" i18n-label label="Cancel" icon="no" (click)="cancelImport(videoImport)"></my-button> |
32 | [routerLink]="getEditVideoUrl(videoImport.video)"></my-edit-button> | 32 | <my-delete-button *ngIf="isVideoImportFailed(videoImport) || isVideoImportCancelled(videoImport) || !videoImport.video" (click)="deleteImport(videoImport)"></my-delete-button> |
33 | <my-edit-button *ngIf="isVideoImportSuccess(videoImport) && videoImport.video" [routerLink]="getEditVideoUrl(videoImport.video)"></my-edit-button> | ||
33 | </td> | 34 | </td> |
34 | 35 | ||
35 | <td> | 36 | <td> |
diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts index 914785bf7..f01558061 100644 --- a/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts +++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts | |||
@@ -37,6 +37,8 @@ export class MyVideoImportsComponent extends RestTable implements OnInit { | |||
37 | return 'badge-banned' | 37 | return 'badge-banned' |
38 | case VideoImportState.PENDING: | 38 | case VideoImportState.PENDING: |
39 | return 'badge-yellow' | 39 | return 'badge-yellow' |
40 | case VideoImportState.PROCESSING: | ||
41 | return 'badge-blue' | ||
40 | default: | 42 | default: |
41 | return 'badge-green' | 43 | return 'badge-green' |
42 | } | 44 | } |
@@ -54,6 +56,10 @@ export class MyVideoImportsComponent extends RestTable implements OnInit { | |||
54 | return videoImport.state.id === VideoImportState.FAILED | 56 | return videoImport.state.id === VideoImportState.FAILED |
55 | } | 57 | } |
56 | 58 | ||
59 | isVideoImportCancelled (videoImport: VideoImport) { | ||
60 | return videoImport.state.id === VideoImportState.CANCELLED | ||
61 | } | ||
62 | |||
57 | getVideoUrl (video: { uuid: string }) { | 63 | getVideoUrl (video: { uuid: string }) { |
58 | return Video.buildWatchUrl(video) | 64 | return Video.buildWatchUrl(video) |
59 | } | 65 | } |
@@ -62,6 +68,24 @@ export class MyVideoImportsComponent extends RestTable implements OnInit { | |||
62 | return Video.buildUpdateUrl(video) | 68 | return Video.buildUpdateUrl(video) |
63 | } | 69 | } |
64 | 70 | ||
71 | deleteImport (videoImport: VideoImport) { | ||
72 | this.videoImportService.deleteVideoImport(videoImport) | ||
73 | .subscribe({ | ||
74 | next: () => this.reloadData(), | ||
75 | |||
76 | error: err => this.notifier.error(err.message) | ||
77 | }) | ||
78 | } | ||
79 | |||
80 | cancelImport (videoImport: VideoImport) { | ||
81 | this.videoImportService.cancelVideoImport(videoImport) | ||
82 | .subscribe({ | ||
83 | next: () => this.reloadData(), | ||
84 | |||
85 | error: err => this.notifier.error(err.message) | ||
86 | }) | ||
87 | } | ||
88 | |||
65 | protected reloadData () { | 89 | protected reloadData () { |
66 | this.videoImportService.getMyVideoImports(this.pagination, this.sort) | 90 | this.videoImportService.getMyVideoImports(this.pagination, this.sort) |
67 | .subscribe({ | 91 | .subscribe({ |
diff --git a/client/src/app/shared/shared-main/buttons/button.component.html b/client/src/app/shared/shared-main/buttons/button.component.html index 65e06f7a4..11c8ffedd 100644 --- a/client/src/app/shared/shared-main/buttons/button.component.html +++ b/client/src/app/shared/shared-main/buttons/button.component.html | |||
@@ -1,5 +1,5 @@ | |||
1 | <span class="action-button" [ngClass]="getClasses()" [ngbTooltip]="getTitle()" tabindex="0"> | 1 | <span class="action-button" [ngClass]="getClasses()" [ngbTooltip]="title" tabindex="0"> |
2 | <my-global-icon *ngIf="!loading" [iconName]="icon"></my-global-icon> | 2 | <my-global-icon *ngIf="icon && !loading" [iconName]="icon"></my-global-icon> |
3 | <my-small-loader [loading]="loading"></my-small-loader> | 3 | <my-small-loader [loading]="loading"></my-small-loader> |
4 | 4 | ||
5 | <span *ngIf="label" class="button-label">{{ label }}</span> | 5 | <span *ngIf="label" class="button-label">{{ label }}</span> |
diff --git a/client/src/app/shared/shared-main/buttons/button.component.ts b/client/src/app/shared/shared-main/buttons/button.component.ts index ee74b3d12..b97012d9a 100644 --- a/client/src/app/shared/shared-main/buttons/button.component.ts +++ b/client/src/app/shared/shared-main/buttons/button.component.ts | |||
@@ -16,10 +16,6 @@ export class ButtonComponent { | |||
16 | @Input() disabled = false | 16 | @Input() disabled = false |
17 | @Input() responsiveLabel = false | 17 | @Input() responsiveLabel = false |
18 | 18 | ||
19 | getTitle () { | ||
20 | return this.title || this.label | ||
21 | } | ||
22 | |||
23 | getClasses () { | 19 | getClasses () { |
24 | return { | 20 | return { |
25 | [this.className]: true, | 21 | [this.className]: true, |
diff --git a/client/src/app/shared/shared-main/buttons/delete-button.component.ts b/client/src/app/shared/shared-main/buttons/delete-button.component.ts index c091f5309..90735852c 100644 --- a/client/src/app/shared/shared-main/buttons/delete-button.component.ts +++ b/client/src/app/shared/shared-main/buttons/delete-button.component.ts | |||
@@ -20,10 +20,6 @@ export class DeleteButtonComponent implements OnInit { | |||
20 | // <my-delete-button label /> Use default label | 20 | // <my-delete-button label /> Use default label |
21 | if (this.label === '') { | 21 | if (this.label === '') { |
22 | this.label = $localize`Delete` | 22 | this.label = $localize`Delete` |
23 | |||
24 | if (!this.title) { | ||
25 | this.title = this.label | ||
26 | } | ||
27 | } | 23 | } |
28 | } | 24 | } |
29 | } | 25 | } |
diff --git a/client/src/app/shared/shared-main/video/video-import.service.ts b/client/src/app/shared/shared-main/video/video-import.service.ts index 99df78e3a..0a610ab1f 100644 --- a/client/src/app/shared/shared-main/video/video-import.service.ts +++ b/client/src/app/shared/shared-main/video/video-import.service.ts | |||
@@ -56,6 +56,16 @@ export class VideoImportService { | |||
56 | ) | 56 | ) |
57 | } | 57 | } |
58 | 58 | ||
59 | deleteVideoImport (videoImport: VideoImport) { | ||
60 | return this.authHttp.delete(VideoImportService.BASE_VIDEO_IMPORT_URL + videoImport.id) | ||
61 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
62 | } | ||
63 | |||
64 | cancelVideoImport (videoImport: VideoImport) { | ||
65 | return this.authHttp.post(VideoImportService.BASE_VIDEO_IMPORT_URL + videoImport.id + '/cancel', {}) | ||
66 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
67 | } | ||
68 | |||
59 | private buildImportVideoObject (video: VideoUpdate): VideoImportCreate { | 69 | private buildImportVideoObject (video: VideoUpdate): VideoImportCreate { |
60 | const language = video.language || null | 70 | const language = video.language || null |
61 | const licence = video.licence || null | 71 | const licence = video.licence || null |
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index a7c4c99c2..c8ec3b4d1 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -257,7 +257,7 @@ | |||
257 | } | 257 | } |
258 | 258 | ||
259 | @mixin peertube-button { | 259 | @mixin peertube-button { |
260 | @include padding(0, 17px, 0, 13px); | 260 | padding: 0 13px; |
261 | 261 | ||
262 | border: 0; | 262 | border: 0; |
263 | font-weight: $font-semibold; | 263 | font-weight: $font-semibold; |
@@ -270,6 +270,10 @@ | |||
270 | 270 | ||
271 | text-align: center; | 271 | text-align: center; |
272 | cursor: pointer; | 272 | cursor: pointer; |
273 | |||
274 | my-global-icon + * { | ||
275 | @include margin-right(4px); | ||
276 | } | ||
273 | } | 277 | } |
274 | 278 | ||
275 | @mixin peertube-button-link { | 279 | @mixin peertube-button-link { |
diff --git a/server/controllers/api/jobs.ts b/server/controllers/api/jobs.ts index eebd195b0..c61b7362f 100644 --- a/server/controllers/api/jobs.ts +++ b/server/controllers/api/jobs.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { Job, JobState, JobType, ResultList, UserRight } from '@shared/models' | 2 | import { HttpStatusCode, Job, JobState, JobType, ResultList, UserRight } from '@shared/models' |
3 | import { isArray } from '../../helpers/custom-validators/misc' | 3 | import { isArray } from '../../helpers/custom-validators/misc' |
4 | import { JobQueue } from '../../lib/job-queue' | 4 | import { JobQueue } from '../../lib/job-queue' |
5 | import { | 5 | import { |
@@ -16,6 +16,18 @@ import { listJobsValidator } from '../../middlewares/validators/jobs' | |||
16 | 16 | ||
17 | const jobsRouter = express.Router() | 17 | const jobsRouter = express.Router() |
18 | 18 | ||
19 | jobsRouter.post('/pause', | ||
20 | authenticate, | ||
21 | ensureUserHasRight(UserRight.MANAGE_JOBS), | ||
22 | asyncMiddleware(pauseJobQueue) | ||
23 | ) | ||
24 | |||
25 | jobsRouter.post('/resume', | ||
26 | authenticate, | ||
27 | ensureUserHasRight(UserRight.MANAGE_JOBS), | ||
28 | asyncMiddleware(resumeJobQueue) | ||
29 | ) | ||
30 | |||
19 | jobsRouter.get('/:state?', | 31 | jobsRouter.get('/:state?', |
20 | openapiOperationDoc({ operationId: 'getJobs' }), | 32 | openapiOperationDoc({ operationId: 'getJobs' }), |
21 | authenticate, | 33 | authenticate, |
@@ -36,6 +48,18 @@ export { | |||
36 | 48 | ||
37 | // --------------------------------------------------------------------------- | 49 | // --------------------------------------------------------------------------- |
38 | 50 | ||
51 | async function pauseJobQueue (req: express.Request, res: express.Response) { | ||
52 | await JobQueue.Instance.pause() | ||
53 | |||
54 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
55 | } | ||
56 | |||
57 | async function resumeJobQueue (req: express.Request, res: express.Response) { | ||
58 | await JobQueue.Instance.resume() | ||
59 | |||
60 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
61 | } | ||
62 | |||
39 | async function listJobs (req: express.Request, res: express.Response) { | 63 | async function listJobs (req: express.Request, res: express.Response) { |
40 | const state = req.params.state as JobState | 64 | const state = req.params.state as JobState |
41 | const asc = req.query.sort === 'createdAt' | 65 | const asc = req.query.sort === 'createdAt' |
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index 08d69827b..8cbfd3286 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts | |||
@@ -19,7 +19,15 @@ import { | |||
19 | MVideoWithBlacklistLight | 19 | MVideoWithBlacklistLight |
20 | } from '@server/types/models' | 20 | } from '@server/types/models' |
21 | import { MVideoImportFormattable } from '@server/types/models/video/video-import' | 21 | import { MVideoImportFormattable } from '@server/types/models/video/video-import' |
22 | import { ServerErrorCode, ThumbnailType, VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '@shared/models' | 22 | import { |
23 | HttpStatusCode, | ||
24 | ServerErrorCode, | ||
25 | ThumbnailType, | ||
26 | VideoImportCreate, | ||
27 | VideoImportState, | ||
28 | VideoPrivacy, | ||
29 | VideoState | ||
30 | } from '@shared/models' | ||
23 | import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' | 31 | import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' |
24 | import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' | 32 | import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' |
25 | import { isArray } from '../../../helpers/custom-validators/misc' | 33 | import { isArray } from '../../../helpers/custom-validators/misc' |
@@ -34,7 +42,14 @@ import { getLocalVideoActivityPubUrl } from '../../../lib/activitypub/url' | |||
34 | import { JobQueue } from '../../../lib/job-queue/job-queue' | 42 | import { JobQueue } from '../../../lib/job-queue/job-queue' |
35 | import { updateVideoMiniatureFromExisting, updateVideoMiniatureFromUrl } from '../../../lib/thumbnail' | 43 | import { updateVideoMiniatureFromExisting, updateVideoMiniatureFromUrl } from '../../../lib/thumbnail' |
36 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | 44 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' |
37 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' | 45 | import { |
46 | asyncMiddleware, | ||
47 | asyncRetryTransactionMiddleware, | ||
48 | authenticate, | ||
49 | videoImportAddValidator, | ||
50 | videoImportCancelValidator, | ||
51 | videoImportDeleteValidator | ||
52 | } from '../../../middlewares' | ||
38 | import { VideoModel } from '../../../models/video/video' | 53 | import { VideoModel } from '../../../models/video/video' |
39 | import { VideoCaptionModel } from '../../../models/video/video-caption' | 54 | import { VideoCaptionModel } from '../../../models/video/video-caption' |
40 | import { VideoImportModel } from '../../../models/video/video-import' | 55 | import { VideoImportModel } from '../../../models/video/video-import' |
@@ -59,6 +74,18 @@ videoImportsRouter.post('/imports', | |||
59 | asyncRetryTransactionMiddleware(addVideoImport) | 74 | asyncRetryTransactionMiddleware(addVideoImport) |
60 | ) | 75 | ) |
61 | 76 | ||
77 | videoImportsRouter.post('/imports/:id/cancel', | ||
78 | authenticate, | ||
79 | asyncMiddleware(videoImportCancelValidator), | ||
80 | asyncRetryTransactionMiddleware(cancelVideoImport) | ||
81 | ) | ||
82 | |||
83 | videoImportsRouter.delete('/imports/:id', | ||
84 | authenticate, | ||
85 | asyncMiddleware(videoImportDeleteValidator), | ||
86 | asyncRetryTransactionMiddleware(deleteVideoImport) | ||
87 | ) | ||
88 | |||
62 | // --------------------------------------------------------------------------- | 89 | // --------------------------------------------------------------------------- |
63 | 90 | ||
64 | export { | 91 | export { |
@@ -67,6 +94,23 @@ export { | |||
67 | 94 | ||
68 | // --------------------------------------------------------------------------- | 95 | // --------------------------------------------------------------------------- |
69 | 96 | ||
97 | async function deleteVideoImport (req: express.Request, res: express.Response) { | ||
98 | const videoImport = res.locals.videoImport | ||
99 | |||
100 | await videoImport.destroy() | ||
101 | |||
102 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
103 | } | ||
104 | |||
105 | async function cancelVideoImport (req: express.Request, res: express.Response) { | ||
106 | const videoImport = res.locals.videoImport | ||
107 | |||
108 | videoImport.state = VideoImportState.CANCELLED | ||
109 | await videoImport.save() | ||
110 | |||
111 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
112 | } | ||
113 | |||
70 | function addVideoImport (req: express.Request, res: express.Response) { | 114 | function addVideoImport (req: express.Request, res: express.Response) { |
71 | if (req.body.targetUrl) return addYoutubeDLImport(req, res) | 115 | if (req.body.targetUrl) return addYoutubeDLImport(req, res) |
72 | 116 | ||
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index b2f511152..6a59bf805 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -441,7 +441,9 @@ const VIDEO_IMPORT_STATES: { [ id in VideoImportState ]: string } = { | |||
441 | [VideoImportState.FAILED]: 'Failed', | 441 | [VideoImportState.FAILED]: 'Failed', |
442 | [VideoImportState.PENDING]: 'Pending', | 442 | [VideoImportState.PENDING]: 'Pending', |
443 | [VideoImportState.SUCCESS]: 'Success', | 443 | [VideoImportState.SUCCESS]: 'Success', |
444 | [VideoImportState.REJECTED]: 'Rejected' | 444 | [VideoImportState.REJECTED]: 'Rejected', |
445 | [VideoImportState.CANCELLED]: 'Cancelled', | ||
446 | [VideoImportState.PROCESSING]: 'Processing' | ||
445 | } | 447 | } |
446 | 448 | ||
447 | const ABUSE_STATES: { [ id in AbuseState ]: string } = { | 449 | const ABUSE_STATES: { [ id in AbuseState ]: string } = { |
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 2f74e9fbd..cb79725aa 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -42,8 +42,17 @@ import { generateVideoMiniature } from '../../thumbnail' | |||
42 | async function processVideoImport (job: Job) { | 42 | async function processVideoImport (job: Job) { |
43 | const payload = job.data as VideoImportPayload | 43 | const payload = job.data as VideoImportPayload |
44 | 44 | ||
45 | if (payload.type === 'youtube-dl') return processYoutubeDLImport(job, payload) | 45 | const videoImport = await getVideoImportOrDie(payload.videoImportId) |
46 | if (payload.type === 'magnet-uri' || payload.type === 'torrent-file') return processTorrentImport(job, payload) | 46 | if (videoImport.state === VideoImportState.CANCELLED) { |
47 | logger.info('Do not process import since it has been cancelled', { payload }) | ||
48 | return | ||
49 | } | ||
50 | |||
51 | videoImport.state = VideoImportState.PROCESSING | ||
52 | await videoImport.save() | ||
53 | |||
54 | if (payload.type === 'youtube-dl') return processYoutubeDLImport(job, videoImport, payload) | ||
55 | if (payload.type === 'magnet-uri' || payload.type === 'torrent-file') return processTorrentImport(job, videoImport, payload) | ||
47 | } | 56 | } |
48 | 57 | ||
49 | // --------------------------------------------------------------------------- | 58 | // --------------------------------------------------------------------------- |
@@ -54,15 +63,11 @@ export { | |||
54 | 63 | ||
55 | // --------------------------------------------------------------------------- | 64 | // --------------------------------------------------------------------------- |
56 | 65 | ||
57 | async function processTorrentImport (job: Job, payload: VideoImportTorrentPayload) { | 66 | async function processTorrentImport (job: Job, videoImport: MVideoImportDefault, payload: VideoImportTorrentPayload) { |
58 | logger.info('Processing torrent video import in job %d.', job.id) | 67 | logger.info('Processing torrent video import in job %d.', job.id) |
59 | 68 | ||
60 | const videoImport = await getVideoImportOrDie(payload.videoImportId) | 69 | const options = { type: payload.type, videoImportId: payload.videoImportId } |
61 | 70 | ||
62 | const options = { | ||
63 | type: payload.type, | ||
64 | videoImportId: payload.videoImportId | ||
65 | } | ||
66 | const target = { | 71 | const target = { |
67 | torrentName: videoImport.torrentName ? getSecureTorrentName(videoImport.torrentName) : undefined, | 72 | torrentName: videoImport.torrentName ? getSecureTorrentName(videoImport.torrentName) : undefined, |
68 | uri: videoImport.magnetUri | 73 | uri: videoImport.magnetUri |
@@ -70,14 +75,10 @@ async function processTorrentImport (job: Job, payload: VideoImportTorrentPayloa | |||
70 | return processFile(() => downloadWebTorrentVideo(target, VIDEO_IMPORT_TIMEOUT), videoImport, options) | 75 | return processFile(() => downloadWebTorrentVideo(target, VIDEO_IMPORT_TIMEOUT), videoImport, options) |
71 | } | 76 | } |
72 | 77 | ||
73 | async function processYoutubeDLImport (job: Job, payload: VideoImportYoutubeDLPayload) { | 78 | async function processYoutubeDLImport (job: Job, videoImport: MVideoImportDefault, payload: VideoImportYoutubeDLPayload) { |
74 | logger.info('Processing youtubeDL video import in job %d.', job.id) | 79 | logger.info('Processing youtubeDL video import in job %d.', job.id) |
75 | 80 | ||
76 | const videoImport = await getVideoImportOrDie(payload.videoImportId) | 81 | const options = { type: payload.type, videoImportId: videoImport.id } |
77 | const options = { | ||
78 | type: payload.type, | ||
79 | videoImportId: videoImport.id | ||
80 | } | ||
81 | 82 | ||
82 | const youtubeDL = new YoutubeDLWrapper(videoImport.targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod')) | 83 | const youtubeDL = new YoutubeDLWrapper(videoImport.targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod')) |
83 | 84 | ||
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index fbc599f12..22bd1f5d2 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -162,6 +162,18 @@ class JobQueue { | |||
162 | } | 162 | } |
163 | } | 163 | } |
164 | 164 | ||
165 | async pause () { | ||
166 | for (const handler of Object.keys(this.queues)) { | ||
167 | await this.queues[handler].pause(true) | ||
168 | } | ||
169 | } | ||
170 | |||
171 | async resume () { | ||
172 | for (const handler of Object.keys(this.queues)) { | ||
173 | await this.queues[handler].resume(true) | ||
174 | } | ||
175 | } | ||
176 | |||
165 | createJob (obj: CreateJobArgument, options: CreateJobOptions = {}): void { | 177 | createJob (obj: CreateJobArgument, options: CreateJobOptions = {}): void { |
166 | this.createJobWithPromise(obj, options) | 178 | this.createJobWithPromise(obj, options) |
167 | .catch(err => logger.error('Cannot create job.', { err, obj })) | 179 | .catch(err => logger.error('Cannot create job.', { err, obj })) |
diff --git a/server/middlewares/validators/videos/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts index e4b54283f..a3a5cc531 100644 --- a/server/middlewares/validators/videos/video-imports.ts +++ b/server/middlewares/validators/videos/video-imports.ts | |||
@@ -1,8 +1,10 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { body } from 'express-validator' | 2 | import { body, param } from 'express-validator' |
3 | import { isValid as isIPValid, parse as parseIP } from 'ipaddr.js' | ||
3 | import { isPreImportVideoAccepted } from '@server/lib/moderation' | 4 | import { isPreImportVideoAccepted } from '@server/lib/moderation' |
4 | import { Hooks } from '@server/lib/plugins/hooks' | 5 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { HttpStatusCode } from '@shared/models' | 6 | import { MUserAccountId, MVideoImport } from '@server/types/models' |
7 | import { HttpStatusCode, UserRight, VideoImportState } from '@shared/models' | ||
6 | import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model' | 8 | import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model' |
7 | import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' | 9 | import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' |
8 | import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' | 10 | import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' |
@@ -11,9 +13,8 @@ import { cleanUpReqFiles } from '../../../helpers/express-utils' | |||
11 | import { logger } from '../../../helpers/logger' | 13 | import { logger } from '../../../helpers/logger' |
12 | import { CONFIG } from '../../../initializers/config' | 14 | import { CONFIG } from '../../../initializers/config' |
13 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' | 15 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' |
14 | import { areValidationErrors, doesVideoChannelOfAccountExist } from '../shared' | 16 | import { areValidationErrors, doesVideoChannelOfAccountExist, doesVideoImportExist } from '../shared' |
15 | import { getCommonVideoEditAttributes } from './videos' | 17 | import { getCommonVideoEditAttributes } from './videos' |
16 | import { isValid as isIPValid, parse as parseIP } from 'ipaddr.js' | ||
17 | 18 | ||
18 | const videoImportAddValidator = getCommonVideoEditAttributes().concat([ | 19 | const videoImportAddValidator = getCommonVideoEditAttributes().concat([ |
19 | body('channelId') | 20 | body('channelId') |
@@ -95,10 +96,58 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([ | |||
95 | } | 96 | } |
96 | ]) | 97 | ]) |
97 | 98 | ||
99 | const videoImportDeleteValidator = [ | ||
100 | param('id') | ||
101 | .custom(isIdValid).withMessage('Should have correct import id'), | ||
102 | |||
103 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
104 | logger.debug('Checking videoImportDeleteValidator parameters', { parameters: req.params }) | ||
105 | |||
106 | if (areValidationErrors(req, res)) return | ||
107 | |||
108 | if (!await doesVideoImportExist(parseInt(req.params.id), res)) return | ||
109 | if (!checkUserCanManageImport(res.locals.oauth.token.user, res.locals.videoImport, res)) return | ||
110 | |||
111 | if (res.locals.videoImport.state === VideoImportState.PENDING) { | ||
112 | return res.fail({ | ||
113 | status: HttpStatusCode.CONFLICT_409, | ||
114 | message: 'Cannot delete a pending video import. Cancel it or wait for the end of the import first.' | ||
115 | }) | ||
116 | } | ||
117 | |||
118 | return next() | ||
119 | } | ||
120 | ] | ||
121 | |||
122 | const videoImportCancelValidator = [ | ||
123 | param('id') | ||
124 | .custom(isIdValid).withMessage('Should have correct import id'), | ||
125 | |||
126 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
127 | logger.debug('Checking videoImportCancelValidator parameters', { parameters: req.params }) | ||
128 | |||
129 | if (areValidationErrors(req, res)) return | ||
130 | |||
131 | if (!await doesVideoImportExist(parseInt(req.params.id), res)) return | ||
132 | if (!checkUserCanManageImport(res.locals.oauth.token.user, res.locals.videoImport, res)) return | ||
133 | |||
134 | if (res.locals.videoImport.state !== VideoImportState.PENDING) { | ||
135 | return res.fail({ | ||
136 | status: HttpStatusCode.CONFLICT_409, | ||
137 | message: 'Cannot cancel a non pending video import.' | ||
138 | }) | ||
139 | } | ||
140 | |||
141 | return next() | ||
142 | } | ||
143 | ] | ||
144 | |||
98 | // --------------------------------------------------------------------------- | 145 | // --------------------------------------------------------------------------- |
99 | 146 | ||
100 | export { | 147 | export { |
101 | videoImportAddValidator | 148 | videoImportAddValidator, |
149 | videoImportCancelValidator, | ||
150 | videoImportDeleteValidator | ||
102 | } | 151 | } |
103 | 152 | ||
104 | // --------------------------------------------------------------------------- | 153 | // --------------------------------------------------------------------------- |
@@ -132,3 +181,15 @@ async function isImportAccepted (req: express.Request, res: express.Response) { | |||
132 | 181 | ||
133 | return true | 182 | return true |
134 | } | 183 | } |
184 | |||
185 | function checkUserCanManageImport (user: MUserAccountId, videoImport: MVideoImport, res: express.Response) { | ||
186 | if (user.hasRight(UserRight.MANAGE_VIDEO_IMPORTS) === false && videoImport.userId !== user.id) { | ||
187 | res.fail({ | ||
188 | status: HttpStatusCode.FORBIDDEN_403, | ||
189 | message: 'Cannot manage video import of another user' | ||
190 | }) | ||
191 | return false | ||
192 | } | ||
193 | |||
194 | return true | ||
195 | } | ||
diff --git a/server/tests/api/check-params/jobs.ts b/server/tests/api/check-params/jobs.ts index d85961d62..801b13d1e 100644 --- a/server/tests/api/check-params/jobs.ts +++ b/server/tests/api/check-params/jobs.ts | |||
@@ -3,7 +3,14 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared' | 4 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared' |
5 | import { HttpStatusCode } from '@shared/models' | 5 | import { HttpStatusCode } from '@shared/models' |
6 | import { cleanupTests, createSingleServer, makeGetRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | 6 | import { |
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | makeGetRequest, | ||
10 | makePostBodyRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@shared/server-commands' | ||
7 | 14 | ||
8 | describe('Test jobs API validators', function () { | 15 | describe('Test jobs API validators', function () { |
9 | const path = '/api/v1/jobs/failed' | 16 | const path = '/api/v1/jobs/failed' |
@@ -76,7 +83,41 @@ describe('Test jobs API validators', function () { | |||
76 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | 83 | expectedStatus: HttpStatusCode.FORBIDDEN_403 |
77 | }) | 84 | }) |
78 | }) | 85 | }) |
86 | }) | ||
87 | |||
88 | describe('When pausing/resuming the job queue', async function () { | ||
89 | const commands = [ 'pause', 'resume' ] | ||
90 | |||
91 | it('Should fail with a non authenticated user', async function () { | ||
92 | for (const command of commands) { | ||
93 | await makePostBodyRequest({ | ||
94 | url: server.url, | ||
95 | path: '/api/v1/jobs/' + command, | ||
96 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
97 | }) | ||
98 | } | ||
99 | }) | ||
79 | 100 | ||
101 | it('Should fail with a non admin user', async function () { | ||
102 | for (const command of commands) { | ||
103 | await makePostBodyRequest({ | ||
104 | url: server.url, | ||
105 | path: '/api/v1/jobs/' + command, | ||
106 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
107 | }) | ||
108 | } | ||
109 | }) | ||
110 | |||
111 | it('Should succeed with the correct params', async function () { | ||
112 | for (const command of commands) { | ||
113 | await makePostBodyRequest({ | ||
114 | url: server.url, | ||
115 | path: '/api/v1/jobs/' + command, | ||
116 | token: server.accessToken, | ||
117 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
118 | }) | ||
119 | } | ||
120 | }) | ||
80 | }) | 121 | }) |
81 | 122 | ||
82 | after(async function () { | 123 | after(async function () { |
diff --git a/server/tests/api/check-params/video-imports.ts b/server/tests/api/check-params/video-imports.ts index da05793a0..156a612ee 100644 --- a/server/tests/api/check-params/video-imports.ts +++ b/server/tests/api/check-params/video-imports.ts | |||
@@ -12,7 +12,9 @@ import { | |||
12 | makePostBodyRequest, | 12 | makePostBodyRequest, |
13 | makeUploadRequest, | 13 | makeUploadRequest, |
14 | PeerTubeServer, | 14 | PeerTubeServer, |
15 | setAccessTokensToServers | 15 | setAccessTokensToServers, |
16 | setDefaultVideoChannel, | ||
17 | waitJobs | ||
16 | } from '@shared/server-commands' | 18 | } from '@shared/server-commands' |
17 | 19 | ||
18 | describe('Test video imports API validator', function () { | 20 | describe('Test video imports API validator', function () { |
@@ -29,6 +31,7 @@ describe('Test video imports API validator', function () { | |||
29 | server = await createSingleServer(1) | 31 | server = await createSingleServer(1) |
30 | 32 | ||
31 | await setAccessTokensToServers([ server ]) | 33 | await setAccessTokensToServers([ server ]) |
34 | await setDefaultVideoChannel([ server ]) | ||
32 | 35 | ||
33 | const username = 'user1' | 36 | const username = 'user1' |
34 | const password = 'my super password' | 37 | const password = 'my super password' |
@@ -347,6 +350,67 @@ describe('Test video imports API validator', function () { | |||
347 | }) | 350 | }) |
348 | }) | 351 | }) |
349 | 352 | ||
353 | describe('Deleting/cancelling a video import', function () { | ||
354 | let importId: number | ||
355 | |||
356 | async function importVideo () { | ||
357 | const attributes = { channelId: server.store.channel.id, targetUrl: FIXTURE_URLS.goodVideo } | ||
358 | const res = await server.imports.importVideo({ attributes }) | ||
359 | |||
360 | return res.id | ||
361 | } | ||
362 | |||
363 | before(async function () { | ||
364 | importId = await importVideo() | ||
365 | }) | ||
366 | |||
367 | it('Should fail with an invalid import id', async function () { | ||
368 | await server.imports.cancel({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
369 | await server.imports.delete({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
370 | }) | ||
371 | |||
372 | it('Should fail with an unknown import id', async function () { | ||
373 | await server.imports.cancel({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
374 | await server.imports.delete({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
375 | }) | ||
376 | |||
377 | it('Should fail without token', async function () { | ||
378 | await server.imports.cancel({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
379 | await server.imports.delete({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
380 | }) | ||
381 | |||
382 | it('Should fail with another user token', async function () { | ||
383 | await server.imports.cancel({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
384 | await server.imports.delete({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
385 | }) | ||
386 | |||
387 | it('Should fail to cancel non pending import', async function () { | ||
388 | this.timeout(60000) | ||
389 | |||
390 | await waitJobs([ server ]) | ||
391 | |||
392 | await server.imports.cancel({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 }) | ||
393 | }) | ||
394 | |||
395 | it('Should succeed to delete an import', async function () { | ||
396 | await server.imports.delete({ importId }) | ||
397 | }) | ||
398 | |||
399 | it('Should fail to delete a pending import', async function () { | ||
400 | await server.jobs.pauseJobQueue() | ||
401 | |||
402 | importId = await importVideo() | ||
403 | |||
404 | await server.imports.delete({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 }) | ||
405 | }) | ||
406 | |||
407 | it('Should succeed to cancel an import', async function () { | ||
408 | importId = await importVideo() | ||
409 | |||
410 | await server.imports.cancel({ importId }) | ||
411 | }) | ||
412 | }) | ||
413 | |||
350 | after(async function () { | 414 | after(async function () { |
351 | await cleanupTests([ server ]) | 415 | await cleanupTests([ server ]) |
352 | }) | 416 | }) |
diff --git a/server/tests/api/server/jobs.ts b/server/tests/api/server/jobs.ts index 4294e1fd5..bd8ffe188 100644 --- a/server/tests/api/server/jobs.ts +++ b/server/tests/api/server/jobs.ts | |||
@@ -11,6 +11,7 @@ import { | |||
11 | setAccessTokensToServers, | 11 | setAccessTokensToServers, |
12 | waitJobs | 12 | waitJobs |
13 | } from '@shared/server-commands' | 13 | } from '@shared/server-commands' |
14 | import { wait } from '@shared/core-utils' | ||
14 | 15 | ||
15 | const expect = chai.expect | 16 | const expect = chai.expect |
16 | 17 | ||
@@ -91,6 +92,30 @@ describe('Test jobs', function () { | |||
91 | expect(jobs.find(j => j.state === 'completed')).to.not.be.undefined | 92 | expect(jobs.find(j => j.state === 'completed')).to.not.be.undefined |
92 | }) | 93 | }) |
93 | 94 | ||
95 | it('Should pause the job queue', async function () { | ||
96 | this.timeout(120000) | ||
97 | |||
98 | await servers[1].jobs.pauseJobQueue() | ||
99 | |||
100 | await servers[1].videos.upload({ attributes: { name: 'video2' } }) | ||
101 | |||
102 | await wait(5000) | ||
103 | |||
104 | const body = await servers[1].jobs.list({ state: 'waiting', jobType: 'video-transcoding' }) | ||
105 | expect(body.data).to.have.lengthOf(1) | ||
106 | }) | ||
107 | |||
108 | it('Should resume the job queue', async function () { | ||
109 | this.timeout(120000) | ||
110 | |||
111 | await servers[1].jobs.resumeJobQueue() | ||
112 | |||
113 | await waitJobs(servers) | ||
114 | |||
115 | const body = await servers[1].jobs.list({ state: 'waiting', jobType: 'video-transcoding' }) | ||
116 | expect(body.data).to.have.lengthOf(0) | ||
117 | }) | ||
118 | |||
94 | after(async function () { | 119 | after(async function () { |
95 | await cleanupTests(servers) | 120 | await cleanupTests(servers) |
96 | }) | 121 | }) |
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts index e8e0f01f1..ba21ab17a 100644 --- a/server/tests/api/videos/video-imports.ts +++ b/server/tests/api/videos/video-imports.ts | |||
@@ -6,7 +6,7 @@ import { pathExists, readdir, remove } from 'fs-extra' | |||
6 | import { join } from 'path' | 6 | import { join } from 'path' |
7 | import { FIXTURE_URLS, testCaptionFile, testImage } from '@server/tests/shared' | 7 | import { FIXTURE_URLS, testCaptionFile, testImage } from '@server/tests/shared' |
8 | import { areHttpImportTestsDisabled } from '@shared/core-utils' | 8 | import { areHttpImportTestsDisabled } from '@shared/core-utils' |
9 | import { VideoPrivacy, VideoResolution } from '@shared/models' | 9 | import { HttpStatusCode, Video, VideoImportState, VideoPrivacy, VideoResolution, VideoState } from '@shared/models' |
10 | import { | 10 | import { |
11 | cleanupTests, | 11 | cleanupTests, |
12 | createMultipleServers, | 12 | createMultipleServers, |
@@ -382,6 +382,85 @@ describe('Test video imports', function () { | |||
382 | 382 | ||
383 | runSuite('yt-dlp') | 383 | runSuite('yt-dlp') |
384 | 384 | ||
385 | describe('Delete/cancel an import', function () { | ||
386 | let server: PeerTubeServer | ||
387 | |||
388 | let finishedImportId: number | ||
389 | let finishedVideo: Video | ||
390 | let pendingImportId: number | ||
391 | |||
392 | async function importVideo (name: string) { | ||
393 | const attributes = { name, channelId: server.store.channel.id, targetUrl: FIXTURE_URLS.goodVideo } | ||
394 | const res = await server.imports.importVideo({ attributes }) | ||
395 | |||
396 | return res.id | ||
397 | } | ||
398 | |||
399 | before(async function () { | ||
400 | this.timeout(120_000) | ||
401 | |||
402 | server = await createSingleServer(1) | ||
403 | |||
404 | await setAccessTokensToServers([ server ]) | ||
405 | await setDefaultVideoChannel([ server ]) | ||
406 | |||
407 | finishedImportId = await importVideo('finished') | ||
408 | await waitJobs([ server ]) | ||
409 | |||
410 | await server.jobs.pauseJobQueue() | ||
411 | pendingImportId = await importVideo('pending') | ||
412 | |||
413 | const { data } = await server.imports.getMyVideoImports() | ||
414 | expect(data).to.have.lengthOf(2) | ||
415 | |||
416 | finishedVideo = data.find(i => i.id === finishedImportId).video | ||
417 | }) | ||
418 | |||
419 | it('Should delete a video import', async function () { | ||
420 | await server.imports.delete({ importId: finishedImportId }) | ||
421 | |||
422 | const { data } = await server.imports.getMyVideoImports() | ||
423 | expect(data).to.have.lengthOf(1) | ||
424 | expect(data[0].id).to.equal(pendingImportId) | ||
425 | expect(data[0].state.id).to.equal(VideoImportState.PENDING) | ||
426 | }) | ||
427 | |||
428 | it('Should not have deleted the associated video', async function () { | ||
429 | const video = await server.videos.get({ id: finishedVideo.id, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
430 | expect(video.name).to.equal('finished') | ||
431 | expect(video.state.id).to.equal(VideoState.PUBLISHED) | ||
432 | }) | ||
433 | |||
434 | it('Should cancel a video import', async function () { | ||
435 | await server.imports.cancel({ importId: pendingImportId }) | ||
436 | |||
437 | const { data } = await server.imports.getMyVideoImports() | ||
438 | expect(data).to.have.lengthOf(1) | ||
439 | expect(data[0].id).to.equal(pendingImportId) | ||
440 | expect(data[0].state.id).to.equal(VideoImportState.CANCELLED) | ||
441 | }) | ||
442 | |||
443 | it('Should not have processed the cancelled video import', async function () { | ||
444 | this.timeout(60_000) | ||
445 | |||
446 | await server.jobs.resumeJobQueue() | ||
447 | |||
448 | await waitJobs([ server ]) | ||
449 | |||
450 | const { data } = await server.imports.getMyVideoImports() | ||
451 | expect(data).to.have.lengthOf(1) | ||
452 | expect(data[0].id).to.equal(pendingImportId) | ||
453 | expect(data[0].state.id).to.equal(VideoImportState.CANCELLED) | ||
454 | expect(data[0].video.state.id).to.equal(VideoState.TO_IMPORT) | ||
455 | }) | ||
456 | |||
457 | it('Should delete the cancelled video import', async function () { | ||
458 | await server.imports.delete({ importId: pendingImportId }) | ||
459 | const { data } = await server.imports.getMyVideoImports() | ||
460 | expect(data).to.have.lengthOf(0) | ||
461 | }) | ||
462 | }) | ||
463 | |||
385 | describe('Auto update', function () { | 464 | describe('Auto update', function () { |
386 | let server: PeerTubeServer | 465 | let server: PeerTubeServer |
387 | 466 | ||
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts index 668535f4e..d3f793d8b 100644 --- a/shared/models/users/user-right.enum.ts +++ b/shared/models/users/user-right.enum.ts | |||
@@ -41,5 +41,7 @@ export const enum UserRight { | |||
41 | MANAGE_VIDEOS_REDUNDANCIES, | 41 | MANAGE_VIDEOS_REDUNDANCIES, |
42 | 42 | ||
43 | MANAGE_VIDEO_FILES, | 43 | MANAGE_VIDEO_FILES, |
44 | RUN_VIDEO_TRANSCODING | 44 | RUN_VIDEO_TRANSCODING, |
45 | |||
46 | MANAGE_VIDEO_IMPORTS | ||
45 | } | 47 | } |
diff --git a/shared/models/videos/import/video-import-state.enum.ts b/shared/models/videos/import/video-import-state.enum.ts index 33dd83f88..ff5c6beff 100644 --- a/shared/models/videos/import/video-import-state.enum.ts +++ b/shared/models/videos/import/video-import-state.enum.ts | |||
@@ -2,5 +2,7 @@ export const enum VideoImportState { | |||
2 | PENDING = 1, | 2 | PENDING = 1, |
3 | SUCCESS = 2, | 3 | SUCCESS = 2, |
4 | FAILED = 3, | 4 | FAILED = 3, |
5 | REJECTED = 4 | 5 | REJECTED = 4, |
6 | CANCELLED = 5, | ||
7 | PROCESSING = 6 | ||
6 | } | 8 | } |
diff --git a/shared/server-commands/server/jobs-command.ts b/shared/server-commands/server/jobs-command.ts index ac62157d1..b8790ea00 100644 --- a/shared/server-commands/server/jobs-command.ts +++ b/shared/server-commands/server/jobs-command.ts | |||
@@ -14,6 +14,30 @@ export class JobsCommand extends AbstractCommand { | |||
14 | return data[0] | 14 | return data[0] |
15 | } | 15 | } |
16 | 16 | ||
17 | pauseJobQueue (options: OverrideCommandOptions = {}) { | ||
18 | const path = '/api/v1/jobs/pause' | ||
19 | |||
20 | return this.postBodyRequest({ | ||
21 | ...options, | ||
22 | |||
23 | path, | ||
24 | implicitToken: true, | ||
25 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
26 | }) | ||
27 | } | ||
28 | |||
29 | resumeJobQueue (options: OverrideCommandOptions = {}) { | ||
30 | const path = '/api/v1/jobs/resume' | ||
31 | |||
32 | return this.postBodyRequest({ | ||
33 | ...options, | ||
34 | |||
35 | path, | ||
36 | implicitToken: true, | ||
37 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
38 | }) | ||
39 | } | ||
40 | |||
17 | list (options: OverrideCommandOptions & { | 41 | list (options: OverrideCommandOptions & { |
18 | state?: JobState | 42 | state?: JobState |
19 | jobType?: JobType | 43 | jobType?: JobType |
diff --git a/shared/server-commands/videos/imports-command.ts b/shared/server-commands/videos/imports-command.ts index e4944694d..f63ed5d4b 100644 --- a/shared/server-commands/videos/imports-command.ts +++ b/shared/server-commands/videos/imports-command.ts | |||
@@ -26,6 +26,34 @@ export class ImportsCommand extends AbstractCommand { | |||
26 | })) | 26 | })) |
27 | } | 27 | } |
28 | 28 | ||
29 | delete (options: OverrideCommandOptions & { | ||
30 | importId: number | ||
31 | }) { | ||
32 | const path = '/api/v1/videos/imports/' + options.importId | ||
33 | |||
34 | return this.deleteRequest({ | ||
35 | ...options, | ||
36 | |||
37 | path, | ||
38 | implicitToken: true, | ||
39 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
40 | }) | ||
41 | } | ||
42 | |||
43 | cancel (options: OverrideCommandOptions & { | ||
44 | importId: number | ||
45 | }) { | ||
46 | const path = '/api/v1/videos/imports/' + options.importId + '/cancel' | ||
47 | |||
48 | return this.postBodyRequest({ | ||
49 | ...options, | ||
50 | |||
51 | path, | ||
52 | implicitToken: true, | ||
53 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
54 | }) | ||
55 | } | ||
56 | |||
29 | getMyVideoImports (options: OverrideCommandOptions & { | 57 | getMyVideoImports (options: OverrideCommandOptions & { |
30 | sort?: string | 58 | sort?: string |
31 | } = {}) { | 59 | } = {}) { |
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 5bf3f13cc..9e721be4b 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml | |||
@@ -252,6 +252,8 @@ tags: | |||
252 | 252 | ||
253 | The import function is practical when the desired video/audio is available online. It makes PeerTube | 253 | The import function is practical when the desired video/audio is available online. It makes PeerTube |
254 | download it for you, saving you as much bandwidth and avoiding any instability or limitation your network might have. | 254 | download it for you, saving you as much bandwidth and avoiding any instability or limitation your network might have. |
255 | - name: Video Imports | ||
256 | description: Operations dealing with listing, adding and removing video imports. | ||
255 | - name: Video Captions | 257 | - name: Video Captions |
256 | description: Operations dealing with listing, adding and removing closed captions of a video. | 258 | description: Operations dealing with listing, adding and removing closed captions of a video. |
257 | - name: Video Channels | 259 | - name: Video Channels |
@@ -306,6 +308,7 @@ x-tagGroups: | |||
306 | tags: | 308 | tags: |
307 | - Video | 309 | - Video |
308 | - Video Upload | 310 | - Video Upload |
311 | - Video Imports | ||
309 | - Video Captions | 312 | - Video Captions |
310 | - Video Channels | 313 | - Video Channels |
311 | - Video Comments | 314 | - Video Comments |
@@ -587,6 +590,30 @@ paths: | |||
587 | '204': | 590 | '204': |
588 | description: successful operation | 591 | description: successful operation |
589 | 592 | ||
593 | /jobs/pause: | ||
594 | post: | ||
595 | summary: Pause job queue | ||
596 | security: | ||
597 | - OAuth2: | ||
598 | - admin | ||
599 | tags: | ||
600 | - Job | ||
601 | responses: | ||
602 | '204': | ||
603 | description: successful operation | ||
604 | |||
605 | /jobs/resume: | ||
606 | post: | ||
607 | summary: Resume job queue | ||
608 | security: | ||
609 | - OAuth2: | ||
610 | - admin | ||
611 | tags: | ||
612 | - Job | ||
613 | responses: | ||
614 | '204': | ||
615 | description: successful operation | ||
616 | |||
590 | /jobs/{state}: | 617 | /jobs/{state}: |
591 | get: | 618 | get: |
592 | summary: List instance jobs | 619 | summary: List instance jobs |
@@ -2166,7 +2193,7 @@ paths: | |||
2166 | security: | 2193 | security: |
2167 | - OAuth2: [] | 2194 | - OAuth2: [] |
2168 | tags: | 2195 | tags: |
2169 | - Video | 2196 | - Video Imports |
2170 | - Video Upload | 2197 | - Video Upload |
2171 | requestBody: | 2198 | requestBody: |
2172 | content: | 2199 | content: |
@@ -2194,6 +2221,34 @@ paths: | |||
2194 | '409': | 2221 | '409': |
2195 | description: HTTP or Torrent/magnetURI import not enabled | 2222 | description: HTTP or Torrent/magnetURI import not enabled |
2196 | 2223 | ||
2224 | /videos/imports/{id}/cancel: | ||
2225 | post: | ||
2226 | summary: Cancel video import | ||
2227 | description: Cancel a pending video import | ||
2228 | security: | ||
2229 | - OAuth2: [] | ||
2230 | tags: | ||
2231 | - Video Imports | ||
2232 | parameters: | ||
2233 | - $ref: '#/components/parameters/id' | ||
2234 | responses: | ||
2235 | '204': | ||
2236 | description: successful operation | ||
2237 | |||
2238 | /videos/imports/{id}: | ||
2239 | delete: | ||
2240 | summary: Delete video import | ||
2241 | description: Delete ended video import | ||
2242 | security: | ||
2243 | - OAuth2: [] | ||
2244 | tags: | ||
2245 | - Video Imports | ||
2246 | parameters: | ||
2247 | - $ref: '#/components/parameters/id' | ||
2248 | responses: | ||
2249 | '204': | ||
2250 | description: successful operation | ||
2251 | |||
2197 | /videos/live: | 2252 | /videos/live: |
2198 | post: | 2253 | post: |
2199 | summary: Create a live | 2254 | summary: Create a live |
@@ -4767,7 +4822,7 @@ components: | |||
4767 | name: id | 4822 | name: id |
4768 | in: path | 4823 | in: path |
4769 | required: true | 4824 | required: true |
4770 | description: The user id | 4825 | description: Entity id |
4771 | schema: | 4826 | schema: |
4772 | $ref: '#/components/schemas/id' | 4827 | $ref: '#/components/schemas/id' |
4773 | idOrUUID: | 4828 | idOrUUID: |