aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.ts6
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts7
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts52
-rw-r--r--client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts12
-rw-r--r--server/controllers/api/videos/transcoding.ts3
-rw-r--r--server/middlewares/validators/videos/video-transcoding.ts13
-rw-r--r--server/tests/api/check-params/transcoding.ts6
-rw-r--r--shared/models/server/server-error-code.enum.ts4
-rw-r--r--shared/models/videos/transcoding/video-transcoding-create.model.ts2
-rw-r--r--shared/server-commands/videos/videos-command.ts7
-rw-r--r--support/doc/api/openapi.yaml4
11 files changed, 88 insertions, 28 deletions
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts
index 52f02d8d0..2792a2d8a 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.ts
+++ b/client/src/app/+admin/overview/videos/video-list.component.ts
@@ -3,7 +3,7 @@ import { finalize } from 'rxjs/operators'
3import { Component, OnInit, ViewChild } from '@angular/core' 3import { Component, OnInit, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' 5import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
6import { formatICU } from '@app/helpers' 6import { formatICU, getAbsoluteAPIUrl } from '@app/helpers'
7import { AdvancedInputFilter } from '@app/shared/shared-forms' 7import { AdvancedInputFilter } from '@app/shared/shared-forms'
8import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 8import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
9import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation' 9import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation'
@@ -166,7 +166,7 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
166 166
167 const files = getAllFiles(video) 167 const files = getAllFiles(video)
168 168
169 return files.some(f => !f.fileUrl.startsWith(window.location.origin)) 169 return files.some(f => !f.fileUrl.startsWith(getAbsoluteAPIUrl()))
170 } 170 }
171 171
172 canRemoveOneFile (video: Video) { 172 canRemoveOneFile (video: Video) {
@@ -294,7 +294,7 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
294 } 294 }
295 295
296 private runTranscoding (videos: Video[], type: 'hls' | 'web-video') { 296 private runTranscoding (videos: Video[], type: 'hls' | 'web-video') {
297 this.videoService.runTranscoding(videos.map(v => v.id), type) 297 this.videoService.runTranscoding({ videoIds: videos.map(v => v.id), type, askForForceTranscodingIfNeeded: false })
298 .subscribe({ 298 .subscribe({
299 next: () => { 299 next: () => {
300 this.notifier.success($localize`Transcoding jobs created.`) 300 this.notifier.success($localize`Transcoding jobs created.`)
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts
index a5bf1db8b..392dcadd0 100644
--- a/client/src/app/shared/shared-main/video/video.model.ts
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -257,9 +257,12 @@ export class Video implements VideoServerModel {
257 } 257 }
258 258
259 canRunTranscoding (user: AuthUser) { 259 canRunTranscoding (user: AuthUser) {
260 return this.canRunForcedTranscoding(user) && this.state.id !== VideoState.TO_TRANSCODE
261 }
262
263 canRunForcedTranscoding (user: AuthUser) {
260 return this.isLocal && 264 return this.isLocal &&
261 user && user.hasRight(UserRight.RUN_VIDEO_TRANSCODING) && 265 user && user.hasRight(UserRight.RUN_VIDEO_TRANSCODING)
262 this.state.id !== VideoState.TO_TRANSCODE
263 } 266 }
264 267
265 hasHLS () { 268 hasHLS () {
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 20145b9c5..cd0a300e2 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -1,9 +1,9 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { from, Observable, of } from 'rxjs' 2import { from, Observable, of, throwError } from 'rxjs'
3import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators' 3import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators'
4import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' 4import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
5import { Injectable } from '@angular/core' 5import { Injectable } from '@angular/core'
6import { AuthService, ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core' 6import { AuthService, ComponentPaginationLight, ConfirmService, RestExtractor, RestService, ServerService, UserService } from '@app/core'
7import { objectToFormData } from '@app/helpers' 7import { objectToFormData } from '@app/helpers'
8import { arrayify } from '@shared/core-utils' 8import { arrayify } from '@shared/core-utils'
9import { 9import {
@@ -11,6 +11,7 @@ import {
11 FeedFormat, 11 FeedFormat,
12 NSFWPolicyType, 12 NSFWPolicyType,
13 ResultList, 13 ResultList,
14 ServerErrorCode,
14 Storyboard, 15 Storyboard,
15 UserVideoRate, 16 UserVideoRate,
16 UserVideoRateType, 17 UserVideoRateType,
@@ -33,8 +34,8 @@ import { AccountService } from '../account/account.service'
33import { VideoChannel, VideoChannelService } from '../video-channel' 34import { VideoChannel, VideoChannelService } from '../video-channel'
34import { VideoDetails } from './video-details.model' 35import { VideoDetails } from './video-details.model'
35import { VideoEdit } from './video-edit.model' 36import { VideoEdit } from './video-edit.model'
36import { Video } from './video.model'
37import { VideoPasswordService } from './video-password.service' 37import { VideoPasswordService } from './video-password.service'
38import { Video } from './video.model'
38 39
39export type CommonVideoParams = { 40export type CommonVideoParams = {
40 videoPagination?: ComponentPaginationLight 41 videoPagination?: ComponentPaginationLight
@@ -64,7 +65,8 @@ export class VideoService {
64 private authHttp: HttpClient, 65 private authHttp: HttpClient,
65 private restExtractor: RestExtractor, 66 private restExtractor: RestExtractor,
66 private restService: RestService, 67 private restService: RestService,
67 private serverService: ServerService 68 private serverService: ServerService,
69 private confirmService: ConfirmService
68 ) {} 70 ) {}
69 71
70 getVideoViewUrl (uuid: string) { 72 getVideoViewUrl (uuid: string) {
@@ -325,17 +327,53 @@ export class VideoService {
325 .pipe(catchError(err => this.restExtractor.handleError(err))) 327 .pipe(catchError(err => this.restExtractor.handleError(err)))
326 } 328 }
327 329
328 runTranscoding (videoIds: (number | string)[], type: 'hls' | 'web-video') { 330 runTranscoding (options: {
329 const body: VideoTranscodingCreate = { transcodingType: type } 331 videoIds: (number | string)[]
332 type: 'hls' | 'web-video'
333 askForForceTranscodingIfNeeded: boolean
334 forceTranscoding?: boolean
335 }): Observable<any> {
336 const { videoIds, type, askForForceTranscodingIfNeeded, forceTranscoding } = options
337
338 if (askForForceTranscodingIfNeeded && videoIds.length !== 1) {
339 throw new Error('Cannot ask to force transcoding on multiple videos')
340 }
341
342 const body: VideoTranscodingCreate = { transcodingType: type, forceTranscoding }
330 343
331 return from(videoIds) 344 return from(videoIds)
332 .pipe( 345 .pipe(
333 concatMap(id => this.authHttp.post(VideoService.BASE_VIDEO_URL + '/' + id + '/transcoding', body)), 346 concatMap(id => {
347 return this.authHttp.post(VideoService.BASE_VIDEO_URL + '/' + id + '/transcoding', body)
348 .pipe(
349 catchError(err => {
350 if (askForForceTranscodingIfNeeded && err.error?.code === ServerErrorCode.VIDEO_ALREADY_BEING_TRANSCODED) {
351 const message = $localize`PeerTube considers this video is already being transcoded.` +
352 // eslint-disable-next-line max-len
353 $localize` If you think PeerTube is wrong (video in broken state after a crash etc.), you can force transcoding on this video.` +
354 ` Do you still want to run transcoding?`
355
356 return from(this.confirmService.confirm(message, $localize`Force transcoding`))
357 .pipe(
358 switchMap(res => {
359 if (res === false) return throwError(() => err)
360
361 return this.runTranscoding({ videoIds, type, askForForceTranscodingIfNeeded: false, forceTranscoding: true })
362 })
363 )
364 }
365
366 return throwError(() => err)
367 })
368 )
369 }),
334 toArray(), 370 toArray(),
335 catchError(err => this.restExtractor.handleError(err)) 371 catchError(err => this.restExtractor.handleError(err))
336 ) 372 )
337 } 373 }
338 374
375 // ---------------------------------------------------------------------------
376
339 loadCompleteDescription (descriptionPath: string) { 377 loadCompleteDescription (descriptionPath: string) {
340 return this.authHttp 378 return this.authHttp
341 .get<{ description: string }>(environment.apiUrl + descriptionPath) 379 .get<{ description: string }>(environment.apiUrl + descriptionPath)
diff --git a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts
index 0a3ada711..9891aae2e 100644
--- a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts
@@ -198,8 +198,8 @@ export class VideoActionsDropdownComponent implements OnChanges {
198 return this.video.canRemoveFiles(this.user) 198 return this.video.canRemoveFiles(this.user)
199 } 199 }
200 200
201 canRunTranscoding () { 201 canRunForcedTranscoding () {
202 return this.video.canRunTranscoding(this.user) 202 return this.video.canRunForcedTranscoding(this.user)
203 } 203 }
204 204
205 /* Action handlers */ 205 /* Action handlers */
@@ -291,10 +291,10 @@ export class VideoActionsDropdownComponent implements OnChanges {
291 } 291 }
292 292
293 runTranscoding (video: Video, type: 'hls' | 'web-video') { 293 runTranscoding (video: Video, type: 'hls' | 'web-video') {
294 this.videoService.runTranscoding([ video.id ], type) 294 this.videoService.runTranscoding({ videoIds: [ video.id ], type, askForForceTranscodingIfNeeded: true })
295 .subscribe({ 295 .subscribe({
296 next: () => { 296 next: () => {
297 this.notifier.success($localize`Transcoding jobs created for ${video.name}.`) 297 this.notifier.success($localize`Transcoding jobs created for "${video.name}".`)
298 this.transcodingCreated.emit() 298 this.transcodingCreated.emit()
299 }, 299 },
300 300
@@ -390,13 +390,13 @@ export class VideoActionsDropdownComponent implements OnChanges {
390 { 390 {
391 label: $localize`Run HLS transcoding`, 391 label: $localize`Run HLS transcoding`,
392 handler: ({ video }) => this.runTranscoding(video, 'hls'), 392 handler: ({ video }) => this.runTranscoding(video, 'hls'),
393 isDisplayed: () => this.displayOptions.transcoding && this.canRunTranscoding(), 393 isDisplayed: () => this.displayOptions.transcoding && this.canRunForcedTranscoding(),
394 iconName: 'cog' 394 iconName: 'cog'
395 }, 395 },
396 { 396 {
397 label: $localize`Run Web Video transcoding`, 397 label: $localize`Run Web Video transcoding`,
398 handler: ({ video }) => this.runTranscoding(video, 'web-video'), 398 handler: ({ video }) => this.runTranscoding(video, 'web-video'),
399 isDisplayed: () => this.displayOptions.transcoding && this.canRunTranscoding(), 399 isDisplayed: () => this.displayOptions.transcoding && this.canRunForcedTranscoding(),
400 iconName: 'cog' 400 iconName: 'cog'
401 }, 401 },
402 { 402 {
diff --git a/server/controllers/api/videos/transcoding.ts b/server/controllers/api/videos/transcoding.ts
index 54f484b2b..c0b93742f 100644
--- a/server/controllers/api/videos/transcoding.ts
+++ b/server/controllers/api/videos/transcoding.ts
@@ -3,6 +3,7 @@ import { logger, loggerTagsFactory } from '@server/helpers/logger'
3import { Hooks } from '@server/lib/plugins/hooks' 3import { Hooks } from '@server/lib/plugins/hooks'
4import { createTranscodingJobs } from '@server/lib/transcoding/create-transcoding-job' 4import { createTranscodingJobs } from '@server/lib/transcoding/create-transcoding-job'
5import { computeResolutionsToTranscode } from '@server/lib/transcoding/transcoding-resolutions' 5import { computeResolutionsToTranscode } from '@server/lib/transcoding/transcoding-resolutions'
6import { VideoJobInfoModel } from '@server/models/video/video-job-info'
6import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models' 7import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models'
7import { asyncMiddleware, authenticate, createTranscodingValidator, ensureUserHasRight } from '../../../middlewares' 8import { asyncMiddleware, authenticate, createTranscodingValidator, ensureUserHasRight } from '../../../middlewares'
8 9
@@ -30,6 +31,8 @@ async function createTranscoding (req: express.Request, res: express.Response) {
30 31
31 const body: VideoTranscodingCreate = req.body 32 const body: VideoTranscodingCreate = req.body
32 33
34 await VideoJobInfoModel.abortAllTasks(video.uuid, 'pendingTranscode')
35
33 const { resolution: maxResolution, hasAudio } = await video.probeMaxQualityFile() 36 const { resolution: maxResolution, hasAudio } = await video.probeMaxQualityFile()
34 37
35 const resolutions = await Hooks.wrapObject( 38 const resolutions = await Hooks.wrapObject(
diff --git a/server/middlewares/validators/videos/video-transcoding.ts b/server/middlewares/validators/videos/video-transcoding.ts
index 3eb2d3141..2f99ff42c 100644
--- a/server/middlewares/validators/videos/video-transcoding.ts
+++ b/server/middlewares/validators/videos/video-transcoding.ts
@@ -1,9 +1,10 @@
1import express from 'express' 1import express from 'express'
2import { body } from 'express-validator' 2import { body } from 'express-validator'
3import { isBooleanValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc'
3import { isValidCreateTranscodingType } from '@server/helpers/custom-validators/video-transcoding' 4import { isValidCreateTranscodingType } from '@server/helpers/custom-validators/video-transcoding'
4import { CONFIG } from '@server/initializers/config' 5import { CONFIG } from '@server/initializers/config'
5import { VideoJobInfoModel } from '@server/models/video/video-job-info' 6import { VideoJobInfoModel } from '@server/models/video/video-job-info'
6import { HttpStatusCode } from '@shared/models' 7import { HttpStatusCode, ServerErrorCode, VideoTranscodingCreate } from '@shared/models'
7import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' 8import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
8 9
9const createTranscodingValidator = [ 10const createTranscodingValidator = [
@@ -12,6 +13,11 @@ const createTranscodingValidator = [
12 body('transcodingType') 13 body('transcodingType')
13 .custom(isValidCreateTranscodingType), 14 .custom(isValidCreateTranscodingType),
14 15
16 body('forceTranscoding')
17 .optional()
18 .customSanitizer(toBooleanOrNull)
19 .custom(isBooleanValid),
20
15 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 21 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
16 if (areValidationErrors(req, res)) return 22 if (areValidationErrors(req, res)) return
17 if (!await doesVideoExist(req.params.videoId, res, 'all')) return 23 if (!await doesVideoExist(req.params.videoId, res, 'all')) return
@@ -32,11 +38,14 @@ const createTranscodingValidator = [
32 }) 38 })
33 } 39 }
34 40
35 // Prefer using job info table instead of video state because before 4.0 failed transcoded video were stuck in "TO_TRANSCODE" state 41 const body = req.body as VideoTranscodingCreate
42 if (body.forceTranscoding === true) return next()
43
36 const info = await VideoJobInfoModel.load(video.id) 44 const info = await VideoJobInfoModel.load(video.id)
37 if (info && info.pendingTranscode > 0) { 45 if (info && info.pendingTranscode > 0) {
38 return res.fail({ 46 return res.fail({
39 status: HttpStatusCode.CONFLICT_409, 47 status: HttpStatusCode.CONFLICT_409,
48 type: ServerErrorCode.VIDEO_ALREADY_BEING_TRANSCODED,
40 message: 'This video is already being transcoded' 49 message: 'This video is already being transcoded'
41 }) 50 })
42 } 51 }
diff --git a/server/tests/api/check-params/transcoding.ts b/server/tests/api/check-params/transcoding.ts
index 4bebcb528..d5899e11b 100644
--- a/server/tests/api/check-params/transcoding.ts
+++ b/server/tests/api/check-params/transcoding.ts
@@ -93,15 +93,17 @@ describe('Test transcoding API validators', function () {
93 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls' }) 93 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls' })
94 await waitJobs(servers) 94 await waitJobs(servers)
95 95
96 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video' }) 96 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', forceTranscoding: true })
97 await waitJobs(servers) 97 await waitJobs(servers)
98 }) 98 })
99 99
100 it('Should not run transcoding on a video that is already being transcoded', async function () { 100 it('Should not run transcoding on a video that is already being transcoded if forceTranscoding is not set', async function () {
101 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video' }) 101 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video' })
102 102
103 const expectedStatus = HttpStatusCode.CONFLICT_409 103 const expectedStatus = HttpStatusCode.CONFLICT_409
104 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus }) 104 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus })
105
106 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', forceTranscoding: true })
105 }) 107 })
106 108
107 after(async function () { 109 after(async function () {
diff --git a/shared/models/server/server-error-code.enum.ts b/shared/models/server/server-error-code.enum.ts
index 77d1e1d3f..583e8245f 100644
--- a/shared/models/server/server-error-code.enum.ts
+++ b/shared/models/server/server-error-code.enum.ts
@@ -52,7 +52,9 @@ export const enum ServerErrorCode {
52 UNKNOWN_RUNNER_TOKEN = 'unknown_runner_token', 52 UNKNOWN_RUNNER_TOKEN = 'unknown_runner_token',
53 53
54 VIDEO_REQUIRES_PASSWORD = 'video_requires_password', 54 VIDEO_REQUIRES_PASSWORD = 'video_requires_password',
55 INCORRECT_VIDEO_PASSWORD = 'incorrect_video_password' 55 INCORRECT_VIDEO_PASSWORD = 'incorrect_video_password',
56
57 VIDEO_ALREADY_BEING_TRANSCODED = 'video_already_being_transcoded'
56} 58}
57 59
58/** 60/**
diff --git a/shared/models/videos/transcoding/video-transcoding-create.model.ts b/shared/models/videos/transcoding/video-transcoding-create.model.ts
index c6e756a0a..6c2dbefa6 100644
--- a/shared/models/videos/transcoding/video-transcoding-create.model.ts
+++ b/shared/models/videos/transcoding/video-transcoding-create.model.ts
@@ -1,3 +1,5 @@
1export interface VideoTranscodingCreate { 1export interface VideoTranscodingCreate {
2 transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7 2 transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7
3
4 forceTranscoding?: boolean // Default false
3} 5}
diff --git a/shared/server-commands/videos/videos-command.ts b/shared/server-commands/videos/videos-command.ts
index a58f1c545..4c3513ed4 100644
--- a/shared/server-commands/videos/videos-command.ts
+++ b/shared/server-commands/videos/videos-command.ts
@@ -775,19 +775,16 @@ export class VideosCommand extends AbstractCommand {
775 }) 775 })
776 } 776 }
777 777
778 runTranscoding (options: OverrideCommandOptions & { 778 runTranscoding (options: OverrideCommandOptions & VideoTranscodingCreate & {
779 videoId: number | string 779 videoId: number | string
780 transcodingType: 'hls' | 'webtorrent' | 'web-video'
781 }) { 780 }) {
782 const path = '/api/v1/videos/' + options.videoId + '/transcoding' 781 const path = '/api/v1/videos/' + options.videoId + '/transcoding'
783 782
784 const fields: VideoTranscodingCreate = pick(options, [ 'transcodingType' ])
785
786 return this.postBodyRequest({ 783 return this.postBodyRequest({
787 ...options, 784 ...options,
788 785
789 path, 786 path,
790 fields, 787 fields: pick(options, [ 'transcodingType', 'forceTranscoding' ]),
791 implicitToken: true, 788 implicitToken: true,
792 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 789 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
793 }) 790 })
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 44daecf85..0cbc58678 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -4914,6 +4914,10 @@ paths:
4914 enum: 4914 enum:
4915 - hls 4915 - hls
4916 - web-video 4916 - web-video
4917 forceTranscoding:
4918 type: boolean
4919 default: false
4920 description: If the video is stuck in transcoding state, do it anyway
4917 required: 4921 required:
4918 - transcodingType 4922 - transcodingType
4919 responses: 4923 responses: