From d23dd9fbfc4d26026352c10f81d2795ceaf2908a Mon Sep 17 00:00:00 2001
From: Chocobozzz <me@florianbigard.com>
Date: Thu, 15 Jul 2021 10:02:54 +0200
Subject: Introduce videos command

---
 shared/extra-utils/miscs/webtorrent.ts         |  16 +-
 shared/extra-utils/requests/requests.ts        |  25 +-
 shared/extra-utils/server/servers.ts           |   5 +-
 shared/extra-utils/shared/abstract-command.ts  |  41 +-
 shared/extra-utils/videos/index.ts             |   1 +
 shared/extra-utils/videos/live-command.ts      |   7 +-
 shared/extra-utils/videos/playlists-command.ts |  17 +-
 shared/extra-utils/videos/videos-command.ts    | 583 ++++++++++++++++++++
 shared/extra-utils/videos/videos.ts            | 714 +------------------------
 9 files changed, 679 insertions(+), 730 deletions(-)
 create mode 100644 shared/extra-utils/videos/videos-command.ts

(limited to 'shared/extra-utils')

diff --git a/shared/extra-utils/miscs/webtorrent.ts b/shared/extra-utils/miscs/webtorrent.ts
index 82548946d..63e648309 100644
--- a/shared/extra-utils/miscs/webtorrent.ts
+++ b/shared/extra-utils/miscs/webtorrent.ts
@@ -1,4 +1,8 @@
+import { readFile } from 'fs-extra'
+import * as parseTorrent from 'parse-torrent'
+import { join } from 'path'
 import * as WebTorrent from 'webtorrent'
+import { ServerInfo } from '../server'
 
 let webtorrent: WebTorrent.Instance
 
@@ -11,6 +15,16 @@ function webtorrentAdd (torrent: string, refreshWebTorrent = false) {
   return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res))
 }
 
+async function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
+  const torrentName = videoUUID + '-' + resolution + '.torrent'
+  const torrentPath = server.serversCommand.buildDirectory(join('torrents', torrentName))
+
+  const data = await readFile(torrentPath)
+
+  return parseTorrent(data)
+}
+
 export {
-  webtorrentAdd
+  webtorrentAdd,
+  parseTorrentVideo
 }
diff --git a/shared/extra-utils/requests/requests.ts b/shared/extra-utils/requests/requests.ts
index c5ee63e05..3f1ac6650 100644
--- a/shared/extra-utils/requests/requests.ts
+++ b/shared/extra-utils/requests/requests.ts
@@ -67,11 +67,17 @@ function makeUploadRequest (options: {
   method?: 'POST' | 'PUT'
   path: string
   token?: string
+
   fields: { [ fieldName: string ]: any }
   attaches?: { [ attachName: string ]: any | any[] }
+
+  headers?: { [ name: string ]: string }
+
   statusCodeExpected?: HttpStatusCode
 }) {
-  if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400
+  if (options.statusCodeExpected === undefined) {
+    options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400
+  }
 
   let req: request.Test
   if (options.method === 'PUT') {
@@ -84,6 +90,10 @@ function makeUploadRequest (options: {
 
   if (options.token) req.set('Authorization', 'Bearer ' + options.token)
 
+  Object.keys(options.headers || {}).forEach(name => {
+    req.set(name, options.headers[name])
+  })
+
   Object.keys(options.fields).forEach(field => {
     const value = options.fields[field]
 
@@ -107,7 +117,11 @@ function makeUploadRequest (options: {
     }
   })
 
-  return req.expect(options.statusCodeExpected)
+  if (options.statusCodeExpected) {
+    req.expect(options.statusCodeExpected)
+  }
+
+  return req
 }
 
 function makePostBodyRequest (options: {
@@ -115,7 +129,9 @@ function makePostBodyRequest (options: {
   path: string
   token?: string
   fields?: { [ fieldName: string ]: any }
+  headers?: { [ name: string ]: string }
   type?: string
+  xForwardedFor?: string
   statusCodeExpected?: HttpStatusCode
 }) {
   if (!options.fields) options.fields = {}
@@ -126,8 +142,13 @@ function makePostBodyRequest (options: {
                 .set('Accept', 'application/json')
 
   if (options.token) req.set('Authorization', 'Bearer ' + options.token)
+  if (options.xForwardedFor) req.set('X-Forwarded-For', options.xForwardedFor)
   if (options.type) req.type(options.type)
 
+  Object.keys(options.headers || {}).forEach(name => {
+    req.set(name, options.headers[name])
+  })
+
   return req.send(options.fields)
             .expect(options.statusCodeExpected)
 }
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts
index b6d597c5d..fda5c3d6d 100644
--- a/shared/extra-utils/server/servers.ts
+++ b/shared/extra-utils/server/servers.ts
@@ -27,7 +27,8 @@ import {
   LiveCommand,
   PlaylistsCommand,
   ServicesCommand,
-  StreamingPlaylistsCommand
+  StreamingPlaylistsCommand,
+  VideosCommand
 } from '../videos'
 import { CommentsCommand } from '../videos/comments-command'
 import { ConfigCommand } from './config-command'
@@ -128,6 +129,7 @@ interface ServerInfo {
   serversCommand?: ServersCommand
   loginCommand?: LoginCommand
   usersCommand?: UsersCommand
+  videosCommand?: VideosCommand
 }
 
 function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) {
@@ -361,6 +363,7 @@ function assignCommands (server: ServerInfo) {
   server.serversCommand = new ServersCommand(server)
   server.loginCommand = new LoginCommand(server)
   server.usersCommand = new UsersCommand(server)
+  server.videosCommand = new VideosCommand(server)
 }
 
 async function reRunServer (server: ServerInfo, configOverride?: any) {
diff --git a/shared/extra-utils/shared/abstract-command.ts b/shared/extra-utils/shared/abstract-command.ts
index af9ecd926..5fddcf639 100644
--- a/shared/extra-utils/shared/abstract-command.ts
+++ b/shared/extra-utils/shared/abstract-command.ts
@@ -1,5 +1,4 @@
 import { isAbsolute, join } from 'path'
-import { HttpStatusCode } from '@shared/core-utils'
 import { root } from '../miscs/tests'
 import {
   makeDeleteRequest,
@@ -38,22 +37,12 @@ interface InternalGetCommandOptions extends InternalCommonCommandOptions {
 
 abstract class AbstractCommand {
 
-  private expectedStatus: HttpStatusCode
-
   constructor (
     protected server: ServerInfo
   ) {
 
   }
 
-  setServer (server: ServerInfo) {
-    this.server = server
-  }
-
-  setExpectedStatus (status: HttpStatusCode) {
-    this.expectedStatus = status
-  }
-
   protected getRequestBody <T> (options: InternalGetCommandOptions) {
     return unwrapBody<T>(this.getRequest(options))
   }
@@ -111,43 +100,51 @@ abstract class AbstractCommand {
 
   protected postBodyRequest (options: InternalCommonCommandOptions & {
     fields?: { [ fieldName: string ]: any }
+    headers?: { [ name: string ]: string }
     type?: string
+    xForwardedFor?: string
   }) {
-    const { type, fields } = options
+    const { type, fields, xForwardedFor, headers } = options
 
     return makePostBodyRequest({
       ...this.buildCommonRequestOptions(options),
 
       fields,
-      type
+      xForwardedFor,
+      type,
+      headers
     })
   }
 
   protected postUploadRequest (options: InternalCommonCommandOptions & {
     fields?: { [ fieldName: string ]: any }
-    attaches?: any
+    attaches?: { [ fieldName: string ]: any }
+    headers?: { [ name: string ]: string }
   }) {
-    const { fields, attaches } = options
+    const { fields, attaches, headers } = options
 
     return makeUploadRequest({
       ...this.buildCommonRequestOptions(options),
 
       method: 'POST',
       fields,
-      attaches
+      attaches,
+      headers
     })
   }
 
   protected putUploadRequest (options: InternalCommonCommandOptions & {
     fields?: { [ fieldName: string ]: any }
-    attaches?: any
+    attaches?: { [ fieldName: string ]: any }
+    headers?: { [ name: string ]: string }
   }) {
-    const { fields, attaches } = options
+    const { fields, attaches, headers } = options
 
     return makeUploadRequest({
       ...this.buildCommonRequestOptions(options),
 
       method: 'PUT',
+      headers,
       fields,
       attaches
     })
@@ -172,7 +169,7 @@ abstract class AbstractCommand {
     })
   }
 
-  private buildCommonRequestOptions (options: InternalCommonCommandOptions) {
+  protected buildCommonRequestOptions (options: InternalCommonCommandOptions) {
     const { url, path } = options
 
     return {
@@ -184,7 +181,7 @@ abstract class AbstractCommand {
     }
   }
 
-  private buildCommonRequestToken (options: Pick<InternalCommonCommandOptions, 'token' | 'implicitToken'>) {
+  protected buildCommonRequestToken (options: Pick<InternalCommonCommandOptions, 'token' | 'implicitToken'>) {
     const { token } = options
 
     const fallbackToken = options.implicitToken
@@ -194,10 +191,10 @@ abstract class AbstractCommand {
     return token !== undefined ? token : fallbackToken
   }
 
-  private buildStatusCodeExpected (options: Pick<InternalCommonCommandOptions, 'expectedStatus' | 'defaultExpectedStatus'>) {
+  protected buildStatusCodeExpected (options: Pick<InternalCommonCommandOptions, 'expectedStatus' | 'defaultExpectedStatus'>) {
     const { expectedStatus, defaultExpectedStatus } = options
 
-    return expectedStatus ?? this.expectedStatus ?? defaultExpectedStatus
+    return expectedStatus !== undefined ? expectedStatus : defaultExpectedStatus
   }
 }
 
diff --git a/shared/extra-utils/videos/index.ts b/shared/extra-utils/videos/index.ts
index 652d82842..26e663f46 100644
--- a/shared/extra-utils/videos/index.ts
+++ b/shared/extra-utils/videos/index.ts
@@ -15,4 +15,5 @@ export * from './services-command'
 export * from './streaming-playlists-command'
 export * from './streaming-playlists'
 export * from './comments-command'
+export * from './videos-command'
 export * from './videos'
diff --git a/shared/extra-utils/videos/live-command.ts b/shared/extra-utils/videos/live-command.ts
index a494e60fa..5adf601cc 100644
--- a/shared/extra-utils/videos/live-command.ts
+++ b/shared/extra-utils/videos/live-command.ts
@@ -9,7 +9,6 @@ import { wait } from '../miscs'
 import { unwrapBody } from '../requests'
 import { AbstractCommand, OverrideCommandOptions } from '../shared'
 import { sendRTMPStream, testFfmpegStreamError } from './live'
-import { getVideoWithToken } from './videos'
 
 export class LiveCommand extends AbstractCommand {
 
@@ -124,8 +123,7 @@ export class LiveCommand extends AbstractCommand {
     let video: VideoDetails
 
     do {
-      const res = await getVideoWithToken(this.server.url, options.token ?? this.server.accessToken, options.videoId)
-      video = res.body
+      video = await this.server.videosCommand.getWithToken({ token: options.token, id: options.videoId })
 
       await wait(500)
     } while (video.isLive === true && video.state.id !== VideoState.PUBLISHED)
@@ -149,8 +147,7 @@ export class LiveCommand extends AbstractCommand {
     let video: VideoDetails
 
     do {
-      const res = await getVideoWithToken(this.server.url, options.token ?? this.server.accessToken, options.videoId)
-      video = res.body
+      video = await this.server.videosCommand.getWithToken({ token: options.token, id: options.videoId })
 
       await wait(500)
     } while (video.state.id !== options.state)
diff --git a/shared/extra-utils/videos/playlists-command.ts b/shared/extra-utils/videos/playlists-command.ts
index f77decc1a..75c8f2433 100644
--- a/shared/extra-utils/videos/playlists-command.ts
+++ b/shared/extra-utils/videos/playlists-command.ts
@@ -1,23 +1,22 @@
 import { omit, pick } from 'lodash'
+import { HttpStatusCode } from '@shared/core-utils'
 import {
   BooleanBothQuery,
   ResultList,
   VideoExistInPlaylist,
   VideoPlaylist,
+  VideoPlaylistCreate,
   VideoPlaylistCreateResult,
   VideoPlaylistElement,
+  VideoPlaylistElementCreate,
   VideoPlaylistElementCreateResult,
-  VideoPlaylistReorder
+  VideoPlaylistElementUpdate,
+  VideoPlaylistReorder,
+  VideoPlaylistType,
+  VideoPlaylistUpdate
 } from '@shared/models'
-import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes'
-import { VideoPlaylistCreate } from '../../models/videos/playlist/video-playlist-create.model'
-import { VideoPlaylistElementCreate } from '../../models/videos/playlist/video-playlist-element-create.model'
-import { VideoPlaylistElementUpdate } from '../../models/videos/playlist/video-playlist-element-update.model'
-import { VideoPlaylistType } from '../../models/videos/playlist/video-playlist-type.model'
-import { VideoPlaylistUpdate } from '../../models/videos/playlist/video-playlist-update.model'
 import { unwrapBody } from '../requests'
 import { AbstractCommand, OverrideCommandOptions } from '../shared'
-import { videoUUIDToId } from './videos'
 
 export class PlaylistsCommand extends AbstractCommand {
 
@@ -185,7 +184,7 @@ export class PlaylistsCommand extends AbstractCommand {
     const attributes = {
       ...options.attributes,
 
-      videoId: await videoUUIDToId(this.server.url, options.attributes.videoId)
+      videoId: await this.server.videosCommand.getId({ ...options, uuid: options.attributes.videoId })
     }
 
     const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
diff --git a/shared/extra-utils/videos/videos-command.ts b/shared/extra-utils/videos/videos-command.ts
new file mode 100644
index 000000000..574705474
--- /dev/null
+++ b/shared/extra-utils/videos/videos-command.ts
@@ -0,0 +1,583 @@
+/* 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, pick } 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 {
+  ResultList,
+  UserVideoRateType,
+  Video,
+  VideoCreate,
+  VideoCreateResult,
+  VideoDetails,
+  VideoFileMetadata,
+  VideoPrivacy,
+  VideosCommonQuery,
+  VideosWithSearchCommonQuery
+} from '@shared/models'
+import { buildAbsoluteFixturePath, wait } from '../miscs'
+import { unwrapBody } from '../requests'
+import { ServerInfo, 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: ServerInfo) {
+    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
+  }
+
+  // ---------------------------------------------------------------------------
+
+  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 & {
+    accountName: string
+  }) {
+    const { accountName, search } = options
+    const path = '/api/v1/accounts/' + accountName + '/videos'
+
+    return this.getRequestBody<ResultList<Video>>({
+      ...options,
+
+      path,
+      query: { search, ...this.buildListQuery(options) },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  listByChannel (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
+    videoChannelName: string
+  }) {
+    const { videoChannelName } = options
+    const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
+
+    return this.getRequestBody<ResultList<Video>>({
+      ...options,
+
+      path,
+      query: this.buildListQuery(options),
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  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 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', expectedStatus } = options
+    let defaultChannelId = 1
+
+    try {
+      const { videoChannels } = await this.server.usersCommand.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 res = mode === 'legacy'
+      ? await this.buildLegacyUpload({ ...options, attributes })
+      : await this.buildResumeUpload({ ...options, attributes })
+
+    // Wait torrent generation
+    if (expectedStatus === HttpStatusCode.OK_200) {
+      let video: VideoDetails
+
+      do {
+        video = await this.getWithToken({ ...options, id: video.uuid })
+
+        await wait(50)
+      } while (!video.files[0].torrentUrl)
+    }
+
+    return res
+  }
+
+  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
+  }) {
+    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'
+      }
+    }
+
+    const initializeSessionRes = await this.prepareResumableUpload({ ...options, 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 })
+
+      return result.body.video
+    }
+
+    const expectedInitStatus = expectedStatus === HttpStatusCode.OK_200
+      ? HttpStatusCode.CREATED_201
+      : expectedStatus
+
+    expect(initStatus).to.equal(expectedInitStatus)
+
+    return initializeSessionRes.body.video as VideoCreateResult
+  }
+
+  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) },
+      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()
+      })
+    })
+  }
+
+  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 }
+
+    if (wait) await waitJobs([ this.server ])
+
+    const result = await this.upload({ ...options, attributes })
+
+    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, [ '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
+  }
+}
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts
index 5e20f8010..19f0df8b8 100644
--- a/shared/extra-utils/videos/videos.ts
+++ b/shared/extra-utils/videos/videos.ts
@@ -1,306 +1,16 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
 
 import { expect } from 'chai'
-import { createReadStream, pathExists, readdir, readFile, stat } from 'fs-extra'
-import got, { Response as GotResponse } from 'got/dist/source'
-import * as parseTorrent from 'parse-torrent'
+import { pathExists, readdir } from 'fs-extra'
 import { join } from 'path'
-import * as request from 'supertest'
-import validator from 'validator'
 import { getLowercaseExtension } from '@server/helpers/core-utils'
-import { buildUUID } from '@server/helpers/uuid'
 import { HttpStatusCode } from '@shared/core-utils'
-import { BooleanBothQuery, VideosCommonQuery } from '@shared/models'
-import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
-import { VideoDetails, VideoPrivacy } from '../../models/videos'
-import { buildAbsoluteFixturePath, dateIsValid, testImage, wait, webtorrentAdd } from '../miscs'
-import { makeGetRequest, makePutBodyRequest, makeRawRequest, makeUploadRequest } from '../requests/requests'
-import { waitJobs } from '../server/jobs'
+import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
+import { dateIsValid, testImage, webtorrentAdd } from '../miscs'
+import { makeRawRequest } from '../requests/requests'
+import { waitJobs } from '../server'
 import { ServerInfo } from '../server/servers'
-import { xxxgetMyUserInformation } from '../users'
-
-loadLanguages()
-
-type VideoAttributes = {
-  name?: string
-  category?: number
-  licence?: number
-  language?: string
-  nsfw?: boolean
-  commentsEnabled?: boolean
-  downloadEnabled?: boolean
-  waitTranscoding?: boolean
-  description?: string
-  originallyPublishedAt?: string
-  tags?: string[]
-  channelId?: number
-  privacy?: VideoPrivacy
-  fixture?: string
-  support?: string
-  thumbnailfile?: string
-  previewfile?: string
-  scheduleUpdate?: {
-    updateAt: string
-    privacy?: VideoPrivacy
-  }
-}
-
-function getVideoCategories (url: string) {
-  const path = '/api/v1/videos/categories'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getVideoLicences (url: string) {
-  const path = '/api/v1/videos/licences'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getVideoLanguages (url: string) {
-  const path = '/api/v1/videos/languages'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getVideoPrivacies (url: string) {
-  const path = '/api/v1/videos/privacies'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
-  const path = '/api/v1/videos/' + id
-
-  return request(url)
-          .get(path)
-          .set('Accept', 'application/json')
-          .expect(expectedStatus)
-}
-
-async function getVideoIdFromUUID (url: string, uuid: string) {
-  const res = await getVideo(url, uuid)
-
-  return res.body.id
-}
-
-function getVideoFileMetadataUrl (url: string) {
-  return request(url)
-    .get('/')
-    .set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function viewVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204, xForwardedFor?: string) {
-  const path = '/api/v1/videos/' + id + '/views'
-
-  const req = request(url)
-    .post(path)
-    .set('Accept', 'application/json')
-
-  if (xForwardedFor) {
-    req.set('X-Forwarded-For', xForwardedFor)
-  }
-
-  return req.expect(expectedStatus)
-}
-
-function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
-  const path = '/api/v1/videos/' + id
-
-  return request(url)
-    .get(path)
-    .set('Authorization', 'Bearer ' + token)
-    .set('Accept', 'application/json')
-    .expect(expectedStatus)
-}
-
-function getVideoDescription (url: string, descriptionPath: string) {
-  return request(url)
-    .get(descriptionPath)
-    .set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function getVideosList (url: string) {
-  const path = '/api/v1/videos'
-
-  return request(url)
-          .get(path)
-          .query({ sort: 'name' })
-          .set('Accept', 'application/json')
-          .expect(HttpStatusCode.OK_200)
-          .expect('Content-Type', /json/)
-}
-
-function getVideosListWithToken (url: string, token: string, query: { nsfw?: BooleanBothQuery } = {}) {
-  const path = '/api/v1/videos'
-
-  return request(url)
-    .get(path)
-    .set('Authorization', 'Bearer ' + token)
-    .query({ sort: 'name', ...query })
-    .set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function getLocalVideos (url: string) {
-  const path = '/api/v1/videos'
-
-  return request(url)
-    .get(path)
-    .query({ sort: 'name', filter: 'local' })
-    .set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string, search?: string) {
-  const path = '/api/v1/users/me/videos'
-
-  const req = request(url)
-    .get(path)
-    .query({ start: start })
-    .query({ count: count })
-    .query({ search: search })
-
-  if (sort) req.query({ sort })
-
-  return req.set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + accessToken)
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function getMyVideosWithFilter (url: string, accessToken: string, query: { isLive?: boolean }) {
-  const path = '/api/v1/users/me/videos'
-
-  return makeGetRequest({
-    url,
-    path,
-    token: accessToken,
-    query,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getAccountVideos (
-  url: string,
-  accessToken: string,
-  accountName: string,
-  start: number,
-  count: number,
-  sort?: string,
-  query: {
-    nsfw?: BooleanBothQuery
-    search?: string
-  } = {}
-) {
-  const path = '/api/v1/accounts/' + accountName + '/videos'
-
-  return makeGetRequest({
-    url,
-    path,
-    query: { ...query, start, count, sort },
-    token: accessToken,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getVideoChannelVideos (
-  url: string,
-  accessToken: string,
-  videoChannelName: string,
-  start: number,
-  count: number,
-  sort?: string,
-  query: { nsfw?: BooleanBothQuery } = {}
-) {
-  const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
-
-  return makeGetRequest({
-    url,
-    path,
-    query: { ...query, start, count, sort },
-    token: accessToken,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getVideosListPagination (url: string, start: number, count: number, sort?: string, skipCount?: boolean) {
-  const path = '/api/v1/videos'
-
-  const req = request(url)
-              .get(path)
-              .query({ start: start })
-              .query({ count: count })
-
-  if (sort) req.query({ sort })
-  if (skipCount) req.query({ skipCount })
-
-  return req.set('Accept', 'application/json')
-           .expect(HttpStatusCode.OK_200)
-           .expect('Content-Type', /json/)
-}
-
-function getVideosListSort (url: string, sort: string) {
-  const path = '/api/v1/videos'
-
-  return request(url)
-          .get(path)
-          .query({ sort: sort })
-          .set('Accept', 'application/json')
-          .expect(HttpStatusCode.OK_200)
-          .expect('Content-Type', /json/)
-}
-
-function getVideosWithFilters (url: string, query: VideosCommonQuery) {
-  const path = '/api/v1/videos'
-
-  return request(url)
-    .get(path)
-    .query(query)
-    .set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function removeVideo (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/videos'
-
-  return request(url)
-          .delete(path + '/' + id)
-          .set('Accept', 'application/json')
-          .set('Authorization', 'Bearer ' + token)
-          .expect(expectedStatus)
-}
-
-async function removeAllVideos (server: ServerInfo) {
-  const resVideos = await getVideosList(server.url)
-
-  for (const v of resVideos.body.data) {
-    await removeVideo(server.url, server.accessToken, v.id)
-  }
-}
+import { VideoEdit } from './videos-command'
 
 async function checkVideoFilesWereRemoved (
   videoUUID: string,
@@ -329,280 +39,20 @@ async function checkVideoFilesWereRemoved (
   }
 }
 
-async function uploadVideo (
-  url: string,
-  accessToken: string,
-  videoAttributesArg: VideoAttributes,
-  specialStatus = HttpStatusCode.OK_200,
-  mode: 'legacy' | 'resumable' = 'legacy'
-) {
-  let defaultChannelId = '1'
-
-  try {
-    const res = await xxxgetMyUserInformation(url, accessToken)
-    defaultChannelId = res.body.videoChannels[0].id
-  } catch (e) { /* empty */ }
-
-  // Override default attributes
-  const attributes = Object.assign({
-    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'
-  }, videoAttributesArg)
-
-  const res = mode === 'legacy'
-    ? await buildLegacyUpload(url, accessToken, attributes, specialStatus)
-    : await buildResumeUpload(url, accessToken, attributes, specialStatus)
-
-  // Wait torrent generation
-  if (specialStatus === HttpStatusCode.OK_200) {
-    let video: VideoDetails
-    do {
-      const resVideo = await getVideoWithToken(url, accessToken, res.body.video.uuid)
-      video = resVideo.body
-
-      await wait(50)
-    } while (!video.files[0].torrentUrl)
-  }
-
-  return res
-}
-
 function checkUploadVideoParam (
-  url: string,
+  server: ServerInfo,
   token: string,
-  attributes: Partial<VideoAttributes>,
-  specialStatus = HttpStatusCode.OK_200,
+  attributes: Partial<VideoEdit>,
+  expectedStatus = HttpStatusCode.OK_200,
   mode: 'legacy' | 'resumable' = 'legacy'
 ) {
   return mode === 'legacy'
-    ? buildLegacyUpload(url, token, attributes, specialStatus)
-    : buildResumeUpload(url, token, attributes, specialStatus)
-}
-
-async function buildLegacyUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
-  const path = '/api/v1/videos/upload'
-  const req = request(url)
-              .post(path)
-              .set('Accept', 'application/json')
-              .set('Authorization', 'Bearer ' + token)
-
-  buildUploadReq(req, attributes)
-
-  if (attributes.fixture !== undefined) {
-    req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
-  }
-
-  return req.expect(specialStatus)
-}
-
-async function buildResumeUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
-  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'
-    }
-  }
-
-  const initializeSessionRes = await prepareResumableUpload({ url, token, 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]
-
-    return sendResumableChunks({ url, token, pathUploadId, videoFilePath, size, specialStatus })
-  }
-
-  const expectedInitStatus = specialStatus === HttpStatusCode.OK_200
-    ? HttpStatusCode.CREATED_201
-    : specialStatus
-
-  expect(initStatus).to.equal(expectedInitStatus)
-
-  return initializeSessionRes
-}
-
-async function prepareResumableUpload (options: {
-  url: string
-  token: string
-  attributes: VideoAttributes
-  size: number
-  mimetype: string
-}) {
-  const { url, token, attributes, size, mimetype } = options
-
-  const path = '/api/v1/videos/upload-resumable'
-
-  const req = request(url)
-              .post(path)
-              .set('Authorization', 'Bearer ' + token)
-              .set('X-Upload-Content-Type', mimetype)
-              .set('X-Upload-Content-Length', size.toString())
-
-  buildUploadReq(req, attributes)
-
-  if (attributes.fixture) {
-    req.field('filename', attributes.fixture)
-  }
-
-  return req
-}
-
-function sendResumableChunks (options: {
-  url: string
-  token: string
-  pathUploadId: string
-  videoFilePath: string
-  size: number
-  specialStatus?: HttpStatusCode
-  contentLength?: number
-  contentRangeBuilder?: (start: number, chunk: any) => string
-}) {
-  const { url, token, pathUploadId, videoFilePath, size, specialStatus, contentLength, contentRangeBuilder } = options
-
-  const expectedStatus = specialStatus || HttpStatusCode.OK_200
-
-  const path = '/api/v1/videos/upload-resumable'
-  let start = 0
-
-  const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
-  return new Promise<GotResponse>((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({
-        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()
-    })
-  })
-}
-
-function updateVideo (
-  url: string,
-  accessToken: string,
-  id: number | string,
-  attributes: VideoAttributes,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/videos/' + id
-  const body = {}
-
-  if (attributes.name) body['name'] = attributes.name
-  if (attributes.category) body['category'] = attributes.category
-  if (attributes.licence) body['licence'] = attributes.licence
-  if (attributes.language) body['language'] = attributes.language
-  if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
-  if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
-  if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
-  if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
-  if (attributes.description) body['description'] = attributes.description
-  if (attributes.tags) body['tags'] = attributes.tags
-  if (attributes.privacy) body['privacy'] = attributes.privacy
-  if (attributes.channelId) body['channelId'] = attributes.channelId
-  if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
-
-  // 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 makeUploadRequest({
-      url,
-      method: 'PUT',
-      path,
-      token: accessToken,
-      fields: body,
-      attaches,
-      statusCodeExpected
-    })
-  }
-
-  return makePutBodyRequest({
-    url,
-    path,
-    fields: body,
-    token: accessToken,
-    statusCodeExpected
-  })
-}
-
-function rateVideo (url: string, accessToken: string, id: number | string, rating: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/videos/' + id + '/rate'
-
-  return request(url)
-          .put(path)
-          .set('Accept', 'application/json')
-          .set('Authorization', 'Bearer ' + accessToken)
-          .send({ rating })
-          .expect(specialStatus)
-}
-
-function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
-  return new Promise<any>((res, rej) => {
-    const torrentName = videoUUID + '-' + resolution + '.torrent'
-    const torrentPath = server.serversCommand.buildDirectory(join('torrents', torrentName))
-
-    readFile(torrentPath, (err, data) => {
-      if (err) return rej(err)
-
-      return res(parseTorrent(data))
-    })
-  })
+    ? server.videosCommand.buildLegacyUpload({ token, attributes, expectedStatus })
+    : server.videosCommand.buildResumeUpload({ token, attributes, expectedStatus })
 }
 
 async function completeVideoCheck (
-  url: string,
+  server: ServerInfo,
   video: any,
   attributes: {
     name: string
@@ -644,7 +94,7 @@ async function completeVideoCheck (
   if (!attributes.likes) attributes.likes = 0
   if (!attributes.dislikes) attributes.dislikes = 0
 
-  const host = new URL(url).host
+  const host = new URL(server.url).host
   const originHost = attributes.account.host
 
   expect(video.name).to.equal(attributes.name)
@@ -681,8 +131,7 @@ async function completeVideoCheck (
     expect(video.originallyPublishedAt).to.be.null
   }
 
-  const res = await getVideo(url, video.uuid)
-  const videoDetails: VideoDetails = res.body
+  const videoDetails = await server.videosCommand.get({ id: video.uuid })
 
   expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
   expect(videoDetails.tags).to.deep.equal(attributes.tags)
@@ -738,148 +187,33 @@ async function completeVideoCheck (
   }
 
   expect(videoDetails.thumbnailPath).to.exist
-  await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
+  await testImage(server.url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
 
   if (attributes.previewfile) {
     expect(videoDetails.previewPath).to.exist
-    await testImage(url, attributes.previewfile, videoDetails.previewPath)
+    await testImage(server.url, attributes.previewfile, videoDetails.previewPath)
   }
 }
 
-async function videoUUIDToId (url: string, id: number | string) {
-  if (validator.isUUID('' + id) === false) return id
-
-  const res = await getVideo(url, id)
-  return res.body.id
-}
-
-async function uploadVideoAndGetId (options: {
-  server: ServerInfo
-  videoName: string
-  nsfw?: boolean
-  privacy?: VideoPrivacy
-  token?: string
-  fixture?: string
-}) {
-  const videoAttrs: any = { name: options.videoName }
-  if (options.nsfw) videoAttrs.nsfw = options.nsfw
-  if (options.privacy) videoAttrs.privacy = options.privacy
-  if (options.fixture) videoAttrs.fixture = options.fixture
-
-  const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
-
-  return res.body.video as { id: number, uuid: string, shortUUID: string }
-}
-
-async function getLocalIdByUUID (url: string, uuid: string) {
-  const res = await getVideo(url, uuid)
-
-  return res.body.id
-}
-
 // serverNumber starts from 1
-async function uploadRandomVideoOnServers (servers: ServerInfo[], serverNumber: number, additionalParams: any = {}) {
+async function uploadRandomVideoOnServers (
+  servers: ServerInfo[],
+  serverNumber: number,
+  additionalParams?: VideoEdit & { prefixName?: string }
+) {
   const server = servers.find(s => s.serverNumber === serverNumber)
-  const res = await uploadRandomVideo(server, false, additionalParams)
+  const res = await server.videosCommand.randomUpload({ wait: false, ...additionalParams })
 
   await waitJobs(servers)
 
   return res
 }
 
-async function uploadRandomVideo (server: ServerInfo, wait = true, additionalParams: any = {}) {
-  const prefixName = additionalParams.prefixName || ''
-  const name = prefixName + buildUUID()
-
-  const data = Object.assign({ name }, additionalParams)
-  const res = await uploadVideo(server.url, server.accessToken, data)
-
-  if (wait) await waitJobs([ server ])
-
-  return { uuid: res.body.video.uuid, name }
-}
-
 // ---------------------------------------------------------------------------
 
 export {
-  getVideoDescription,
-  getVideoCategories,
-  uploadRandomVideo,
-  getVideoLicences,
-  videoUUIDToId,
-  getVideoPrivacies,
-  getVideoLanguages,
-  getMyVideos,
-  getAccountVideos,
-  getVideoChannelVideos,
-  getVideo,
-  getVideoFileMetadataUrl,
-  getVideoWithToken,
-  getVideosList,
-  removeAllVideos,
   checkUploadVideoParam,
-  getVideosListPagination,
-  getVideosListSort,
-  removeVideo,
-  getVideosListWithToken,
-  uploadVideo,
-  sendResumableChunks,
-  getVideosWithFilters,
-  uploadRandomVideoOnServers,
-  updateVideo,
-  rateVideo,
-  viewVideo,
-  parseTorrentVideo,
-  getLocalVideos,
   completeVideoCheck,
-  checkVideoFilesWereRemoved,
-  getMyVideosWithFilter,
-  uploadVideoAndGetId,
-  getLocalIdByUUID,
-  getVideoIdFromUUID,
-  prepareResumableUpload
-}
-
-// ---------------------------------------------------------------------------
-
-function buildUploadReq (req: request.Test, attributes: VideoAttributes) {
-
-  for (const key of [ 'name', 'support', 'channelId', 'description', 'originallyPublishedAt' ]) {
-    if (attributes[key] !== undefined) {
-      req.field(key, attributes[key])
-    }
-  }
-
-  for (const key of [ 'nsfw', 'commentsEnabled', 'downloadEnabled', 'waitTranscoding' ]) {
-    if (attributes[key] !== undefined) {
-      req.field(key, JSON.stringify(attributes[key]))
-    }
-  }
-
-  for (const key of [ 'language', 'privacy', 'category', 'licence' ]) {
-    if (attributes[key] !== undefined) {
-      req.field(key, attributes[key].toString())
-    }
-  }
-
-  const tags = attributes.tags || []
-  for (let i = 0; i < tags.length; i++) {
-    req.field('tags[' + i + ']', attributes.tags[i])
-  }
-
-  for (const key of [ 'thumbnailfile', 'previewfile' ]) {
-    if (attributes[key] !== undefined) {
-      req.attach(key, buildAbsoluteFixturePath(attributes[key]))
-    }
-  }
-
-  if (attributes.scheduleUpdate) {
-    if (attributes.scheduleUpdate.updateAt) {
-      req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
-    }
-
-    if (attributes.scheduleUpdate.privacy) {
-      req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
-    }
-  }
+  uploadRandomVideoOnServers,
+  checkVideoFilesWereRemoved
 }
-- 
cgit v1.2.3