mirror of https://github.com/Chocobozzz/PeerTube
				
				
				
			
		
			
				
	
	
		
			628 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			628 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			TypeScript
		
	
	
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
 | 
						|
 | 
						|
import { expect } from 'chai'
 | 
						|
import { createReadStream, stat } from 'fs-extra'
 | 
						|
import got, { Response as GotResponse } from 'got'
 | 
						|
import { omit } from 'lodash'
 | 
						|
import validator from 'validator'
 | 
						|
import { buildUUID } from '@server/helpers/uuid'
 | 
						|
import { loadLanguages } from '@server/initializers/constants'
 | 
						|
import { pick } from '@shared/core-utils'
 | 
						|
import {
 | 
						|
  HttpStatusCode,
 | 
						|
  ResultList,
 | 
						|
  UserVideoRateType,
 | 
						|
  Video,
 | 
						|
  VideoCreate,
 | 
						|
  VideoCreateResult,
 | 
						|
  VideoDetails,
 | 
						|
  VideoFileMetadata,
 | 
						|
  VideoPrivacy,
 | 
						|
  VideosCommonQuery,
 | 
						|
  VideosWithSearchCommonQuery
 | 
						|
} from '@shared/models'
 | 
						|
import { buildAbsoluteFixturePath, wait } from '../miscs'
 | 
						|
import { unwrapBody } from '../requests'
 | 
						|
import { PeerTubeServer, waitJobs } from '../server'
 | 
						|
import { AbstractCommand, OverrideCommandOptions } from '../shared'
 | 
						|
 | 
						|
export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
 | 
						|
  fixture?: string
 | 
						|
  thumbnailfile?: string
 | 
						|
  previewfile?: string
 | 
						|
}
 | 
						|
 | 
						|
export class VideosCommand extends AbstractCommand {
 | 
						|
 | 
						|
  constructor (server: PeerTubeServer) {
 | 
						|
    super(server)
 | 
						|
 | 
						|
    loadLanguages()
 | 
						|
  }
 | 
						|
 | 
						|
  getCategories (options: OverrideCommandOptions = {}) {
 | 
						|
    const path = '/api/v1/videos/categories'
 | 
						|
 | 
						|
    return this.getRequestBody<{ [id: number]: string }>({
 | 
						|
      ...options,
 | 
						|
      path,
 | 
						|
 | 
						|
      implicitToken: false,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.OK_200
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  getLicences (options: OverrideCommandOptions = {}) {
 | 
						|
    const path = '/api/v1/videos/licences'
 | 
						|
 | 
						|
    return this.getRequestBody<{ [id: number]: string }>({
 | 
						|
      ...options,
 | 
						|
      path,
 | 
						|
 | 
						|
      implicitToken: false,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.OK_200
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  getLanguages (options: OverrideCommandOptions = {}) {
 | 
						|
    const path = '/api/v1/videos/languages'
 | 
						|
 | 
						|
    return this.getRequestBody<{ [id: string]: string }>({
 | 
						|
      ...options,
 | 
						|
      path,
 | 
						|
 | 
						|
      implicitToken: false,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.OK_200
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  getPrivacies (options: OverrideCommandOptions = {}) {
 | 
						|
    const path = '/api/v1/videos/privacies'
 | 
						|
 | 
						|
    return this.getRequestBody<{ [id in VideoPrivacy]: string }>({
 | 
						|
      ...options,
 | 
						|
      path,
 | 
						|
 | 
						|
      implicitToken: false,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.OK_200
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  // ---------------------------------------------------------------------------
 | 
						|
 | 
						|
  getDescription (options: OverrideCommandOptions & {
 | 
						|
    descriptionPath: string
 | 
						|
  }) {
 | 
						|
    return this.getRequestBody<{ description: string }>({
 | 
						|
      ...options,
 | 
						|
      path: options.descriptionPath,
 | 
						|
 | 
						|
      implicitToken: false,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.OK_200
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  getFileMetadata (options: OverrideCommandOptions & {
 | 
						|
    url: string
 | 
						|
  }) {
 | 
						|
    return unwrapBody<VideoFileMetadata>(this.getRawRequest({
 | 
						|
      ...options,
 | 
						|
 | 
						|
      url: options.url,
 | 
						|
      implicitToken: false,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.OK_200
 | 
						|
    }))
 | 
						|
  }
 | 
						|
 | 
						|
  // ---------------------------------------------------------------------------
 | 
						|
 | 
						|
  view (options: OverrideCommandOptions & {
 | 
						|
    id: number | string
 | 
						|
    xForwardedFor?: string
 | 
						|
  }) {
 | 
						|
    const { id, xForwardedFor } = options
 | 
						|
    const path = '/api/v1/videos/' + id + '/views'
 | 
						|
 | 
						|
    return this.postBodyRequest({
 | 
						|
      ...options,
 | 
						|
 | 
						|
      path,
 | 
						|
      xForwardedFor,
 | 
						|
      implicitToken: false,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  rate (options: OverrideCommandOptions & {
 | 
						|
    id: number | string
 | 
						|
    rating: UserVideoRateType
 | 
						|
  }) {
 | 
						|
    const { id, rating } = options
 | 
						|
    const path = '/api/v1/videos/' + id + '/rate'
 | 
						|
 | 
						|
    return this.putBodyRequest({
 | 
						|
      ...options,
 | 
						|
 | 
						|
      path,
 | 
						|
      fields: { rating },
 | 
						|
      implicitToken: true,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  // ---------------------------------------------------------------------------
 | 
						|
 | 
						|
  get (options: OverrideCommandOptions & {
 | 
						|
    id: number | string
 | 
						|
  }) {
 | 
						|
    const path = '/api/v1/videos/' + options.id
 | 
						|
 | 
						|
    return this.getRequestBody<VideoDetails>({
 | 
						|
      ...options,
 | 
						|
 | 
						|
      path,
 | 
						|
      implicitToken: false,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.OK_200
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  getWithToken (options: OverrideCommandOptions & {
 | 
						|
    id: number | string
 | 
						|
  }) {
 | 
						|
    return this.get({
 | 
						|
      ...options,
 | 
						|
 | 
						|
      token: this.buildCommonRequestToken({ ...options, implicitToken: true })
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  async getId (options: OverrideCommandOptions & {
 | 
						|
    uuid: number | string
 | 
						|
  }) {
 | 
						|
    const { uuid } = options
 | 
						|
 | 
						|
    if (validator.isUUID('' + uuid) === false) return uuid as number
 | 
						|
 | 
						|
    const { id } = await this.get({ ...options, id: uuid })
 | 
						|
 | 
						|
    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 & {
 | 
						|
    start?: number
 | 
						|
    count?: number
 | 
						|
    sort?: string
 | 
						|
    search?: string
 | 
						|
    isLive?: boolean
 | 
						|
  } = {}) {
 | 
						|
    const path = '/api/v1/users/me/videos'
 | 
						|
 | 
						|
    return this.getRequestBody<ResultList<Video>>({
 | 
						|
      ...options,
 | 
						|
 | 
						|
      path,
 | 
						|
      query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive' ]),
 | 
						|
      implicitToken: true,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.OK_200
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  // ---------------------------------------------------------------------------
 | 
						|
 | 
						|
  list (options: OverrideCommandOptions & VideosCommonQuery = {}) {
 | 
						|
    const path = '/api/v1/videos'
 | 
						|
 | 
						|
    const query = this.buildListQuery(options)
 | 
						|
 | 
						|
    return this.getRequestBody<ResultList<Video>>({
 | 
						|
      ...options,
 | 
						|
 | 
						|
      path,
 | 
						|
      query: { sort: 'name', ...query },
 | 
						|
      implicitToken: false,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.OK_200
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  listWithToken (options: OverrideCommandOptions & VideosCommonQuery = {}) {
 | 
						|
    return this.list({
 | 
						|
      ...options,
 | 
						|
 | 
						|
      token: this.buildCommonRequestToken({ ...options, implicitToken: true })
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  listByAccount (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
 | 
						|
    handle: string
 | 
						|
  }) {
 | 
						|
    const { handle, search } = options
 | 
						|
    const path = '/api/v1/accounts/' + handle + '/videos'
 | 
						|
 | 
						|
    return this.getRequestBody<ResultList<Video>>({
 | 
						|
      ...options,
 | 
						|
 | 
						|
      path,
 | 
						|
      query: { search, ...this.buildListQuery(options) },
 | 
						|
      implicitToken: true,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.OK_200
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  listByChannel (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
 | 
						|
    handle: string
 | 
						|
  }) {
 | 
						|
    const { handle } = options
 | 
						|
    const path = '/api/v1/video-channels/' + handle + '/videos'
 | 
						|
 | 
						|
    return this.getRequestBody<ResultList<Video>>({
 | 
						|
      ...options,
 | 
						|
 | 
						|
      path,
 | 
						|
      query: this.buildListQuery(options),
 | 
						|
      implicitToken: true,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.OK_200
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  // ---------------------------------------------------------------------------
 | 
						|
 | 
						|
  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
 | 
						|
  }) {
 | 
						|
    const { id, attributes = {} } = options
 | 
						|
    const path = '/api/v1/videos/' + id
 | 
						|
 | 
						|
    // Upload request
 | 
						|
    if (attributes.thumbnailfile || attributes.previewfile) {
 | 
						|
      const attaches: any = {}
 | 
						|
      if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
 | 
						|
      if (attributes.previewfile) attaches.previewfile = attributes.previewfile
 | 
						|
 | 
						|
      return this.putUploadRequest({
 | 
						|
        ...options,
 | 
						|
 | 
						|
        path,
 | 
						|
        fields: options.attributes,
 | 
						|
        attaches: {
 | 
						|
          thumbnailfile: attributes.thumbnailfile,
 | 
						|
          previewfile: attributes.previewfile
 | 
						|
        },
 | 
						|
        implicitToken: true,
 | 
						|
        defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
 | 
						|
      })
 | 
						|
    }
 | 
						|
 | 
						|
    return this.putBodyRequest({
 | 
						|
      ...options,
 | 
						|
 | 
						|
      path,
 | 
						|
      fields: options.attributes,
 | 
						|
      implicitToken: true,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  remove (options: OverrideCommandOptions & {
 | 
						|
    id: number | string
 | 
						|
  }) {
 | 
						|
    const path = '/api/v1/videos/' + options.id
 | 
						|
 | 
						|
    return unwrapBody(this.deleteRequest({
 | 
						|
      ...options,
 | 
						|
 | 
						|
      path,
 | 
						|
      implicitToken: true,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
 | 
						|
    }))
 | 
						|
  }
 | 
						|
 | 
						|
  async removeAll () {
 | 
						|
    const { data } = await this.list()
 | 
						|
 | 
						|
    for (const v of data) {
 | 
						|
      await this.remove({ id: v.id })
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // ---------------------------------------------------------------------------
 | 
						|
 | 
						|
  async upload (options: OverrideCommandOptions & {
 | 
						|
    attributes?: VideoEdit
 | 
						|
    mode?: 'legacy' | 'resumable' // default legacy
 | 
						|
  } = {}) {
 | 
						|
    const { mode = 'legacy' } = options
 | 
						|
    let defaultChannelId = 1
 | 
						|
 | 
						|
    try {
 | 
						|
      const { videoChannels } = await this.server.users.getMyInfo({ token: options.token })
 | 
						|
      defaultChannelId = videoChannels[0].id
 | 
						|
    } catch (e) { /* empty */ }
 | 
						|
 | 
						|
    // Override default attributes
 | 
						|
    const attributes = {
 | 
						|
      name: 'my super video',
 | 
						|
      category: 5,
 | 
						|
      licence: 4,
 | 
						|
      language: 'zh',
 | 
						|
      channelId: defaultChannelId,
 | 
						|
      nsfw: true,
 | 
						|
      waitTranscoding: false,
 | 
						|
      description: 'my super description',
 | 
						|
      support: 'my super support text',
 | 
						|
      tags: [ 'tag' ],
 | 
						|
      privacy: VideoPrivacy.PUBLIC,
 | 
						|
      commentsEnabled: true,
 | 
						|
      downloadEnabled: true,
 | 
						|
      fixture: 'video_short.webm',
 | 
						|
 | 
						|
      ...options.attributes
 | 
						|
    }
 | 
						|
 | 
						|
    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: created.uuid })
 | 
						|
 | 
						|
        await wait(50)
 | 
						|
      } while (!video.files[0].torrentUrl)
 | 
						|
    }
 | 
						|
 | 
						|
    return created
 | 
						|
  }
 | 
						|
 | 
						|
  async buildLegacyUpload (options: OverrideCommandOptions & {
 | 
						|
    attributes: VideoEdit
 | 
						|
  }): Promise<VideoCreateResult> {
 | 
						|
    const path = '/api/v1/videos/upload'
 | 
						|
 | 
						|
    return unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
 | 
						|
      ...options,
 | 
						|
 | 
						|
      path,
 | 
						|
      fields: this.buildUploadFields(options.attributes),
 | 
						|
      attaches: this.buildUploadAttaches(options.attributes),
 | 
						|
      implicitToken: true,
 | 
						|
      defaultExpectedStatus: HttpStatusCode.OK_200
 | 
						|
    })).then(body => body.video || body as any)
 | 
						|
  }
 | 
						|
 | 
						|
  async buildResumeUpload (options: OverrideCommandOptions & {
 | 
						|
    attributes: VideoEdit
 | 
						|
  }): Promise<VideoCreateResult> {
 | 
						|
    const { attributes, expectedStatus } = options
 | 
						|
 | 
						|
    let size = 0
 | 
						|
    let videoFilePath: string
 | 
						|
    let mimetype = 'video/mp4'
 | 
						|
 | 
						|
    if (attributes.fixture) {
 | 
						|
      videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
 | 
						|
      size = (await stat(videoFilePath)).size
 | 
						|
 | 
						|
      if (videoFilePath.endsWith('.mkv')) {
 | 
						|
        mimetype = 'video/x-matroska'
 | 
						|
      } else if (videoFilePath.endsWith('.webm')) {
 | 
						|
        mimetype = 'video/webm'
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // 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) {
 | 
						|
      const locationHeader = initializeSessionRes.header['location']
 | 
						|
      expect(locationHeader).to.not.be.undefined
 | 
						|
 | 
						|
      const pathUploadId = locationHeader.split('?')[1]
 | 
						|
 | 
						|
      const result = await this.sendResumableChunks({ ...options, pathUploadId, videoFilePath, size })
 | 
						|
 | 
						|
      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
 | 
						|
      ? HttpStatusCode.CREATED_201
 | 
						|
      : expectedStatus
 | 
						|
 | 
						|
    expect(initStatus).to.equal(expectedInitStatus)
 | 
						|
 | 
						|
    return initializeSessionRes.body.video || initializeSessionRes.body
 | 
						|
  }
 | 
						|
 | 
						|
  async prepareResumableUpload (options: OverrideCommandOptions & {
 | 
						|
    attributes: VideoEdit
 | 
						|
    size: number
 | 
						|
    mimetype: string
 | 
						|
  }) {
 | 
						|
    const { attributes, size, mimetype } = options
 | 
						|
 | 
						|
    const path = '/api/v1/videos/upload-resumable'
 | 
						|
 | 
						|
    return this.postUploadRequest({
 | 
						|
      ...options,
 | 
						|
 | 
						|
      path,
 | 
						|
      headers: {
 | 
						|
        'X-Upload-Content-Type': mimetype,
 | 
						|
        'X-Upload-Content-Length': size.toString()
 | 
						|
      },
 | 
						|
      fields: { filename: attributes.fixture, ...this.buildUploadFields(options.attributes) },
 | 
						|
      // Fixture will be sent later
 | 
						|
      attaches: this.buildUploadAttaches(omit(options.attributes, 'fixture')),
 | 
						|
      implicitToken: true,
 | 
						|
 | 
						|
      defaultExpectedStatus: null
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  sendResumableChunks (options: OverrideCommandOptions & {
 | 
						|
    pathUploadId: string
 | 
						|
    videoFilePath: string
 | 
						|
    size: number
 | 
						|
    contentLength?: number
 | 
						|
    contentRangeBuilder?: (start: number, chunk: any) => string
 | 
						|
  }) {
 | 
						|
    const { pathUploadId, videoFilePath, size, contentLength, contentRangeBuilder, expectedStatus = HttpStatusCode.OK_200 } = options
 | 
						|
 | 
						|
    const path = '/api/v1/videos/upload-resumable'
 | 
						|
    let start = 0
 | 
						|
 | 
						|
    const token = this.buildCommonRequestToken({ ...options, implicitToken: true })
 | 
						|
    const url = this.server.url
 | 
						|
 | 
						|
    const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
 | 
						|
    return new Promise<GotResponse<{ video: VideoCreateResult }>>((resolve, reject) => {
 | 
						|
      readable.on('data', async function onData (chunk) {
 | 
						|
        readable.pause()
 | 
						|
 | 
						|
        const headers = {
 | 
						|
          'Authorization': 'Bearer ' + token,
 | 
						|
          'Content-Type': 'application/octet-stream',
 | 
						|
          'Content-Range': contentRangeBuilder
 | 
						|
            ? contentRangeBuilder(start, chunk)
 | 
						|
            : `bytes ${start}-${start + chunk.length - 1}/${size}`,
 | 
						|
          'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
 | 
						|
        }
 | 
						|
 | 
						|
        const res = await got<{ video: VideoCreateResult }>({
 | 
						|
          url,
 | 
						|
          method: 'put',
 | 
						|
          headers,
 | 
						|
          path: path + '?' + pathUploadId,
 | 
						|
          body: chunk,
 | 
						|
          responseType: 'json',
 | 
						|
          throwHttpErrors: false
 | 
						|
        })
 | 
						|
 | 
						|
        start += chunk.length
 | 
						|
 | 
						|
        if (res.statusCode === expectedStatus) {
 | 
						|
          return resolve(res)
 | 
						|
        }
 | 
						|
 | 
						|
        if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
 | 
						|
          readable.off('data', onData)
 | 
						|
          return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
 | 
						|
        }
 | 
						|
 | 
						|
        readable.resume()
 | 
						|
      })
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  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
 | 
						|
    privacy?: VideoPrivacy
 | 
						|
    fixture?: string
 | 
						|
  }) {
 | 
						|
    const attributes: VideoEdit = { name: options.name }
 | 
						|
    if (options.nsfw) attributes.nsfw = options.nsfw
 | 
						|
    if (options.privacy) attributes.privacy = options.privacy
 | 
						|
    if (options.fixture) attributes.fixture = options.fixture
 | 
						|
 | 
						|
    return this.upload({ ...options, attributes })
 | 
						|
  }
 | 
						|
 | 
						|
  async randomUpload (options: OverrideCommandOptions & {
 | 
						|
    wait?: boolean // default true
 | 
						|
    additionalParams?: VideoEdit & { prefixName?: string }
 | 
						|
  } = {}) {
 | 
						|
    const { wait = true, additionalParams } = options
 | 
						|
    const prefixName = additionalParams?.prefixName || ''
 | 
						|
    const name = prefixName + buildUUID()
 | 
						|
 | 
						|
    const attributes = { name, ...additionalParams }
 | 
						|
 | 
						|
    const result = await this.upload({ ...options, attributes })
 | 
						|
 | 
						|
    if (wait) await waitJobs([ this.server ])
 | 
						|
 | 
						|
    return { ...result, name }
 | 
						|
  }
 | 
						|
 | 
						|
  // ---------------------------------------------------------------------------
 | 
						|
 | 
						|
  private buildListQuery (options: VideosCommonQuery) {
 | 
						|
    return pick(options, [
 | 
						|
      'start',
 | 
						|
      'count',
 | 
						|
      'sort',
 | 
						|
      'nsfw',
 | 
						|
      'isLive',
 | 
						|
      'categoryOneOf',
 | 
						|
      'licenceOneOf',
 | 
						|
      'languageOneOf',
 | 
						|
      'tagsOneOf',
 | 
						|
      'tagsAllOf',
 | 
						|
      'filter',
 | 
						|
      'skipCount'
 | 
						|
    ])
 | 
						|
  }
 | 
						|
 | 
						|
  private buildUploadFields (attributes: VideoEdit) {
 | 
						|
    return omit(attributes, [ 'fixture', 'thumbnailfile', 'previewfile' ])
 | 
						|
  }
 | 
						|
 | 
						|
  private buildUploadAttaches (attributes: VideoEdit) {
 | 
						|
    const attaches: { [ name: string ]: string } = {}
 | 
						|
 | 
						|
    for (const key of [ 'thumbnailfile', 'previewfile' ]) {
 | 
						|
      if (attributes[key]) attaches[key] = buildAbsoluteFixturePath(attributes[key])
 | 
						|
    }
 | 
						|
 | 
						|
    if (attributes.fixture) attaches.videofile = buildAbsoluteFixturePath(attributes.fixture)
 | 
						|
 | 
						|
    return attaches
 | 
						|
  }
 | 
						|
}
 |