]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - shared/extra-utils/videos/videos-command.ts
Add ability to run transcoding jobs
[github/Chocobozzz/PeerTube.git] / shared / extra-utils / videos / videos-command.ts
index 5556cddf6c7da4a0c5379c701a1267c6b23023b0..7ec9c3647cbc67a419d67c5e2454014de5887771 100644 (file)
@@ -3,12 +3,13 @@
 import { expect } from 'chai'
 import { createReadStream, stat } from 'fs-extra'
 import got, { Response as GotResponse } from 'got'
-import { omit, pick } from 'lodash'
+import { omit } from 'lodash'
 import validator from 'validator'
 import { buildUUID } from '@server/helpers/uuid'
 import { loadLanguages } from '@server/initializers/constants'
-import { HttpStatusCode } from '@shared/core-utils'
+import { pick } from '@shared/core-utils'
 import {
+  HttpStatusCode,
   ResultList,
   UserVideoRateType,
   Video,
@@ -18,11 +19,11 @@ import {
   VideoFileMetadata,
   VideoPrivacy,
   VideosCommonQuery,
-  VideosWithSearchCommonQuery
+  VideoTranscodingCreate
 } from '@shared/models'
 import { buildAbsoluteFixturePath, wait } from '../miscs'
 import { unwrapBody } from '../requests'
-import { ServerInfo, waitJobs } from '../server'
+import { PeerTubeServer, waitJobs } from '../server'
 import { AbstractCommand, OverrideCommandOptions } from '../shared'
 
 export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
@@ -33,7 +34,7 @@ export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile
 
 export class VideosCommand extends AbstractCommand {
 
-  constructor (server: ServerInfo) {
+  constructor (server: PeerTubeServer) {
     super(server)
 
     loadLanguages()
@@ -187,6 +188,17 @@ export class VideosCommand extends AbstractCommand {
     return id
   }
 
+  async listFiles (options: OverrideCommandOptions & {
+    id: number | string
+  }) {
+    const video = await this.get(options)
+
+    const files = video.files || []
+    const hlsFiles = video.streamingPlaylists[0]?.files || []
+
+    return files.concat(hlsFiles)
+  }
+
   // ---------------------------------------------------------------------------
 
   listMyVideos (options: OverrideCommandOptions & {
@@ -195,6 +207,7 @@ export class VideosCommand extends AbstractCommand {
     sort?: string
     search?: string
     isLive?: boolean
+    channelId?: number
   } = {}) {
     const path = '/api/v1/users/me/videos'
 
@@ -202,7 +215,7 @@ export class VideosCommand extends AbstractCommand {
       ...options,
 
       path,
-      query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive' ]),
+      query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive', 'channelId' ]),
       implicitToken: true,
       defaultExpectedStatus: HttpStatusCode.OK_200
     })
@@ -233,11 +246,11 @@ export class VideosCommand extends AbstractCommand {
     })
   }
 
-  listByAccount (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
-    accountName: string
+  listByAccount (options: OverrideCommandOptions & VideosCommonQuery & {
+    handle: string
   }) {
-    const { accountName, search } = options
-    const path = '/api/v1/accounts/' + accountName + '/videos'
+    const { handle, search } = options
+    const path = '/api/v1/accounts/' + handle + '/videos'
 
     return this.getRequestBody<ResultList<Video>>({
       ...options,
@@ -249,11 +262,11 @@ export class VideosCommand extends AbstractCommand {
     })
   }
 
-  listByChannel (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
-    videoChannelName: string
+  listByChannel (options: OverrideCommandOptions & VideosCommonQuery & {
+    handle: string
   }) {
-    const { videoChannelName } = options
-    const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
+    const { handle } = options
+    const path = '/api/v1/video-channels/' + handle + '/videos'
 
     return this.getRequestBody<ResultList<Video>>({
       ...options,
@@ -267,6 +280,16 @@ export class VideosCommand extends AbstractCommand {
 
   // ---------------------------------------------------------------------------
 
+  async find (options: OverrideCommandOptions & {
+    name: string
+  }) {
+    const { data } = await this.list(options)
+
+    return data.find(v => v.name === options.name)
+  }
+
+  // ---------------------------------------------------------------------------
+
   update (options: OverrideCommandOptions & {
     id: number | string
     attributes?: VideoEdit
@@ -309,13 +332,13 @@ export class VideosCommand extends AbstractCommand {
   }) {
     const path = '/api/v1/videos/' + options.id
 
-    return this.deleteRequest({
+    return unwrapBody(this.deleteRequest({
       ...options,
 
       path,
       implicitToken: true,
       defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
-    })
+    }))
   }
 
   async removeAll () {
@@ -332,7 +355,7 @@ export class VideosCommand extends AbstractCommand {
     attributes?: VideoEdit
     mode?: 'legacy' | 'resumable' // default legacy
   } = {}) {
-    const { mode = 'legacy', expectedStatus } = options
+    const { mode = 'legacy' } = options
     let defaultChannelId = 1
 
     try {
@@ -360,22 +383,23 @@ export class VideosCommand extends AbstractCommand {
       ...options.attributes
     }
 
-    const res = mode === 'legacy'
+    const created = mode === 'legacy'
       ? await this.buildLegacyUpload({ ...options, attributes })
       : await this.buildResumeUpload({ ...options, attributes })
 
     // Wait torrent generation
+    const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 })
     if (expectedStatus === HttpStatusCode.OK_200) {
       let video: VideoDetails
 
       do {
-        video = await this.getWithToken({ ...options, id: video.uuid })
+        video = await this.getWithToken({ ...options, id: created.uuid })
 
         await wait(50)
       } while (!video.files[0].torrentUrl)
     }
 
-    return res
+    return created
   }
 
   async buildLegacyUpload (options: OverrideCommandOptions & {
@@ -396,7 +420,7 @@ export class VideosCommand extends AbstractCommand {
 
   async buildResumeUpload (options: OverrideCommandOptions & {
     attributes: VideoEdit
-  }) {
+  }): Promise<VideoCreateResult> {
     const { attributes, expectedStatus } = options
 
     let size = 0
@@ -414,7 +438,8 @@ export class VideosCommand extends AbstractCommand {
       }
     }
 
-    const initializeSessionRes = await this.prepareResumableUpload({ ...options, attributes, size, mimetype })
+    // Do not check status automatically, we'll check it manually
+    const initializeSessionRes = await this.prepareResumableUpload({ ...options, expectedStatus: null, attributes, size, mimetype })
     const initStatus = initializeSessionRes.status
 
     if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
@@ -425,7 +450,11 @@ export class VideosCommand extends AbstractCommand {
 
       const result = await this.sendResumableChunks({ ...options, pathUploadId, videoFilePath, size })
 
-      return result.body.video
+      if (result.statusCode === HttpStatusCode.OK_200) {
+        await this.endResumableUpload({ ...options, expectedStatus: HttpStatusCode.NO_CONTENT_204, pathUploadId })
+      }
+
+      return result.body?.video || result.body as any
     }
 
     const expectedInitStatus = expectedStatus === HttpStatusCode.OK_200
@@ -434,15 +463,18 @@ export class VideosCommand extends AbstractCommand {
 
     expect(initStatus).to.equal(expectedInitStatus)
 
-    return initializeSessionRes.body.video as VideoCreateResult
+    return initializeSessionRes.body.video || initializeSessionRes.body
   }
 
   async prepareResumableUpload (options: OverrideCommandOptions & {
     attributes: VideoEdit
     size: number
     mimetype: string
+
+    originalName?: string
+    lastModified?: number
   }) {
-    const { attributes, size, mimetype } = options
+    const { attributes, originalName, lastModified, size, mimetype } = options
 
     const path = '/api/v1/videos/upload-resumable'
 
@@ -454,8 +486,18 @@ export class VideosCommand extends AbstractCommand {
         'X-Upload-Content-Type': mimetype,
         'X-Upload-Content-Length': size.toString()
       },
-      fields: { filename: attributes.fixture, ...this.buildUploadFields(options.attributes) },
+      fields: {
+        filename: attributes.fixture,
+        originalName,
+        lastModified,
+
+        ...this.buildUploadFields(options.attributes)
+      },
+
+      // Fixture will be sent later
+      attaches: this.buildUploadAttaches(omit(options.attributes, 'fixture')),
       implicitToken: true,
+
       defaultExpectedStatus: null
     })
   }
@@ -515,6 +557,19 @@ export class VideosCommand extends AbstractCommand {
     })
   }
 
+  endResumableUpload (options: OverrideCommandOptions & {
+    pathUploadId: string
+  }) {
+    return this.deleteRequest({
+      ...options,
+
+      path: '/api/v1/videos/upload-resumable',
+      rawQuery: options.pathUploadId,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
   quickUpload (options: OverrideCommandOptions & {
     name: string
     nsfw?: boolean
@@ -531,23 +586,71 @@ export class VideosCommand extends AbstractCommand {
 
   async randomUpload (options: OverrideCommandOptions & {
     wait?: boolean // default true
-    additionalParams?: VideoEdit & { prefixName: string }
+    additionalParams?: VideoEdit & { prefixName?: string }
   } = {}) {
     const { wait = true, additionalParams } = options
     const prefixName = additionalParams?.prefixName || ''
     const name = prefixName + buildUUID()
 
-    const attributes = { name, additionalParams }
-
-    if (wait) await waitJobs([ this.server ])
+    const attributes = { name, ...additionalParams }
 
     const result = await this.upload({ ...options, attributes })
 
+    if (wait) await waitJobs([ this.server ])
+
     return { ...result, name }
   }
 
   // ---------------------------------------------------------------------------
 
+  removeHLSFiles (options: OverrideCommandOptions & {
+    videoId: number | string
+  }) {
+    const path = '/api/v1/videos/' + options.videoId + '/hls'
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  removeWebTorrentFiles (options: OverrideCommandOptions & {
+    videoId: number | string
+  }) {
+    const path = '/api/v1/videos/' + options.videoId + '/webtorrent'
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  runTranscoding (options: OverrideCommandOptions & {
+    videoId: number | string
+    transcodingType: 'hls' | 'webtorrent'
+  }) {
+    const path = '/api/v1/videos/' + options.videoId + '/transcoding'
+
+    const fields: VideoTranscodingCreate = pick(options, [ 'transcodingType' ])
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
   private buildListQuery (options: VideosCommonQuery) {
     return pick(options, [
       'start',
@@ -560,13 +663,14 @@ export class VideosCommand extends AbstractCommand {
       'languageOneOf',
       'tagsOneOf',
       'tagsAllOf',
-      'filter',
+      'isLocal',
+      'include',
       'skipCount'
     ])
   }
 
   private buildUploadFields (attributes: VideoEdit) {
-    return omit(attributes, [ 'thumbnailfile', 'previewfile' ])
+    return omit(attributes, [ 'fixture', 'thumbnailfile', 'previewfile' ])
   }
 
   private buildUploadAttaches (attributes: VideoEdit) {