one cli to unite them all
authorRigel Kent <sendmemail@rigelk.eu>
Thu, 13 Sep 2018 12:27:44 +0000 (14:27 +0200)
committerRigel Kent <sendmemail@rigelk.eu>
Fri, 14 Sep 2018 09:08:55 +0000 (11:08 +0200)
Ash nazg thrakatulûk agh burzum-ishi krimpatul

- refactor import-videos to use the youtubeDL helper
- add very basic tests for the cli

16 files changed:
package.json
server/helpers/youtube-dl.ts
server/tests/cli/index.ts
server/tests/cli/peertube.ts [new file with mode: 0644]
server/tools/cli.ts [new file with mode: 0644]
server/tools/get-access-token.ts [deleted file]
server/tools/import-videos.ts [deleted file]
server/tools/peertube-auth.ts [new file with mode: 0644]
server/tools/peertube-get-access-token.ts [new file with mode: 0644]
server/tools/peertube-import-videos.ts [new file with mode: 0644]
server/tools/peertube-upload.ts [new file with mode: 0644]
server/tools/peertube-watch.ts [new file with mode: 0644]
server/tools/peertube.ts [new file with mode: 0755]
server/tools/upload.ts [deleted file]
support/doc/tools.md
tsconfig.json

index 5a8843b0c5f984f148a313f204b997e0f8f9b6f9..cc4f6be5ceb484f682b40ca00f6475cc000f1104 100644 (file)
@@ -7,6 +7,9 @@
   "engines": {
     "node": ">=8.x"
   },
+  "bin": {
+    "peertube": "dist/server/tools/peertube.js"
+  },
   "author": {
     "name": "Florian Bigard",
     "email": "florian.bigard@gmail.com",
@@ -78,6 +81,7 @@
     "@types/bluebird": "3.5.21"
   },
   "dependencies": {
+    "application-config": "^1.0.1",
     "async": "^2.0.0",
     "async-lock": "^1.1.2",
     "async-lru": "^1.1.1",
@@ -86,6 +90,7 @@
     "bluebird": "^3.5.0",
     "body-parser": "^1.12.4",
     "bull": "^3.4.2",
+    "cli-table": "^0.3.1",
     "bytes": "^3.0.0",
     "commander": "^2.13.0",
     "concurrently": "^4.0.1",
     "magnet-uri": "^5.1.4",
     "morgan": "^1.5.3",
     "multer": "^1.1.0",
+    "netrc-parser": "^3.1.6",
     "nodemailer": "^4.4.2",
     "parse-torrent": "^6.0.0",
     "password-generator": "^2.0.2",
     "sequelize-typescript": "0.6.6",
     "sharp": "^0.20.0",
     "srt-to-vtt": "^1.1.2",
+    "summon-install": "^0.4.3",
     "useragent": "^2.3.0",
     "uuid": "^3.1.0",
     "validator": "^10.2.0",
   "scripty": {
     "silent": true
   },
+  "summon": {
+    "silent": true
+  },
   "sasslintConfig": "client/.sass-lint.yml"
 }
index 6738090f3613c91d996b48ecfd00e37f03c16782..8b2bc17824f511866ffc1509dc7fa8afc71b7b43 100644 (file)
@@ -14,9 +14,9 @@ export type YoutubeDLInfo = {
   thumbnailUrl?: string
 }
 
-function getYoutubeDLInfo (url: string): Promise<YoutubeDLInfo> {
+function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> {
   return new Promise<YoutubeDLInfo>(async (res, rej) => {
-    const options = [ '-j', '--flat-playlist' ]
+    const options = opts || [ '-j', '--flat-playlist' ]
 
     const youtubeDL = await safeGetYoutubeDL()
     youtubeDL.getInfo(url, options, (err, info) => {
@@ -48,15 +48,6 @@ function downloadYoutubeDLVideo (url: string) {
   })
 }
 
-// ---------------------------------------------------------------------------
-
-export {
-  downloadYoutubeDLVideo,
-  getYoutubeDLInfo
-}
-
-// ---------------------------------------------------------------------------
-
 async function safeGetYoutubeDL () {
   let youtubeDL
 
@@ -71,6 +62,16 @@ async function safeGetYoutubeDL () {
   return youtubeDL
 }
 
+// ---------------------------------------------------------------------------
+
+export {
+  downloadYoutubeDLVideo,
+  getYoutubeDLInfo,
+  safeGetYoutubeDL
+}
+
+// ---------------------------------------------------------------------------
+
 function normalizeObject (obj: any) {
   const newObj: any = {}
 
index f99eafe03c74b16f3eeb19956367450a12b4629f..33e33a0701597a6af6a7c261709e0cb99fdc3d47 100644 (file)
@@ -1,5 +1,6 @@
 // Order of the tests we want to execute
 import './create-transcoding-job'
 import './create-import-video-file-job'
+import './peertube'
 import './reset-password'
 import './update-host'
diff --git a/server/tests/cli/peertube.ts b/server/tests/cli/peertube.ts
new file mode 100644 (file)
index 0000000..548fd12
--- /dev/null
@@ -0,0 +1,51 @@
+import 'mocha'
+import {
+  expect
+} from 'chai'
+import {
+  createUser,
+  execCLI,
+  flushTests,
+  getEnvCli,
+  killallServers,
+  runServer,
+  ServerInfo,
+  setAccessTokensToServers
+} from '../utils'
+
+describe('Test CLI wrapper', function () {
+  let server: ServerInfo
+  const cmd = 'node ./dist/server/tools/peertube.js'
+
+  before(async function () {
+    this.timeout(30000)
+
+    await flushTests()
+    server = await runServer(1)
+    await setAccessTokensToServers([ server ])
+
+    await createUser(server.url, server.accessToken, 'user_1', 'super password')
+  })
+
+  it('Should display no selected instance', async function () {
+    this.timeout(60000)
+
+    const env = getEnvCli(server)
+    const stdout = await execCLI(`${env} ${cmd} --help`)
+
+    expect(stdout).to.contain('selected')
+  })
+
+  it('Should remember the authentifying material of the user', async function () {
+    this.timeout(60000)
+
+    const env = getEnvCli(server)
+    const stdout = await execCLI(`${env} ` + cmd + ` auth add --url ${server.url} -U user_1 -p "super password"`)
+  })
+
+  after(async function () {
+    await execCLI(cmd + ` auth del ${server.url}`)
+
+    killallServers([ server ])
+  })
+})
diff --git a/server/tools/cli.ts b/server/tools/cli.ts
new file mode 100644 (file)
index 0000000..9a170d4
--- /dev/null
@@ -0,0 +1,63 @@
+const config = require('application-config')('PeerTube/CLI')
+const netrc = require('netrc-parser').default
+
+const version = () => {
+  const tag = require('child_process')
+    .execSync('[[ ! -d .git ]] || git name-rev --name-only --tags --no-undefined HEAD 2>/dev/null || true', { stdio: [0,1,2] })
+  if (tag) return tag
+
+  const version = require('child_process')
+    .execSync('[[ ! -d .git ]] || git rev-parse --short HEAD').toString().trim()
+  if (version) return version
+
+  return require('../../../package.json').version
+}
+
+let settings = {
+  remotes: [],
+  default: 0
+}
+
+interface Settings {
+  remotes: any[],
+  default: number
+}
+
+async function getSettings () {
+  return new Promise<Settings>((res, rej) => {
+    let settings = {
+      remotes: [],
+      default: 0
+    } as Settings
+    config.read((err, data) => {
+      if (err) {
+        return rej(err)
+      }
+      return res(data || settings)
+    })
+  })
+}
+
+async function writeSettings (settings) {
+  return new Promise((res, rej) => {
+    config.write(settings, function (err) {
+      if (err) {
+        return rej(err)
+      }
+      return res()
+    })
+  })
+}
+
+netrc.loadSync()
+
+// ---------------------------------------------------------------------------
+
+export {
+  version,
+  config,
+  settings,
+  getSettings,
+  writeSettings,
+  netrc
+}
diff --git a/server/tools/get-access-token.ts b/server/tools/get-access-token.ts
deleted file mode 100644 (file)
index d86c84c..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-import * as program from 'commander'
-
-import {
-  getClient,
-  serverLogin,
-  Server,
-  Client,
-  User
-} from '../tests/utils/index'
-
-program
-  .option('-u, --url <url>', 'Server url')
-  .option('-n, --username <username>', 'Username')
-  .option('-p, --password <token>', 'Password')
-  .parse(process.argv)
-
-if (
-  !program['url'] ||
-  !program['username'] ||
-  !program['password']
-) {
-  throw new Error('All arguments are required.')
-}
-
-getClient(program.url)
-  .then(res => {
-    const server = {
-      url: program['url'],
-      user: {
-        username: program['username'],
-        password: program['password']
-      } as User,
-      client: {
-        id: res.body.client_id as string,
-        secret: res.body.client_secret as string
-      } as Client
-    } as Server
-
-    return serverLogin(server)
-  })
-  .then(accessToken => {
-    console.log(accessToken)
-    process.exit(0)
-  })
-  .catch(err => {
-    console.error(err)
-    process.exit(-1)
-  })
diff --git a/server/tools/import-videos.ts b/server/tools/import-videos.ts
deleted file mode 100644 (file)
index 3ff194c..0000000
+++ /dev/null
@@ -1,286 +0,0 @@
-// FIXME: https://github.com/nodejs/node/pull/16853
-require('tls').DEFAULT_ECDH_CURVE = 'auto'
-
-import * as program from 'commander'
-import { join } from 'path'
-import * as youtubeDL from 'youtube-dl'
-import { VideoPrivacy } from '../../shared/models/videos'
-import { doRequestAndSaveToFile } from '../helpers/requests'
-import { CONSTRAINTS_FIELDS } from '../initializers'
-import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../tests/utils'
-import { truncate } from 'lodash'
-import * as prompt from 'prompt'
-import { remove } from 'fs-extra'
-
-program
-  .option('-u, --url <url>', 'Server url')
-  .option('-U, --username <username>', 'Username')
-  .option('-p, --password <token>', 'Password')
-  .option('-t, --target-url <targetUrl>', 'Video target URL')
-  .option('-l, --language <languageCode>', 'Language ISO 639 code (fr or en...)')
-  .option('-v, --verbose', 'Verbose mode')
-  .parse(process.argv)
-
-if (
-  !program['url'] ||
-  !program['username'] ||
-  !program['targetUrl']
-) {
-  console.error('All arguments are required.')
-  process.exit(-1)
-}
-
-const user = {
-  username: program['username'],
-  password: program['password']
-}
-
-run().catch(err => console.error(err))
-
-let accessToken: string
-let client: { id: string, secret: string }
-
-const processOptions = {
-  cwd: __dirname,
-  maxBuffer: Infinity
-}
-
-async function promptPassword () {
-  return new Promise((res, rej) => {
-    prompt.start()
-    const schema = {
-      properties: {
-        password: {
-          hidden: true,
-          required: true
-        }
-      }
-    }
-    prompt.get(schema, function (err, result) {
-      if (err) {
-        return rej(err)
-      }
-      return res(result.password)
-    })
-  })
-}
-
-async function run () {
-  if (!user.password) {
-    user.password = await promptPassword()
-  }
-
-  const res = await getClient(program['url'])
-  client = {
-    id: res.body.client_id,
-    secret: res.body.client_secret
-  }
-
-  const res2 = await login(program['url'], client, user)
-  accessToken = res2.body.access_token
-
-  const options = [ '-j', '--flat-playlist', '--playlist-reverse' ]
-  youtubeDL.getInfo(program['targetUrl'], options, processOptions, async (err, info) => {
-    if (err) {
-      console.log(err.message)
-      process.exit(1)
-    }
-
-    let infoArray: any[]
-
-    // Normalize utf8 fields
-    if (Array.isArray(info) === true) {
-      infoArray = info.map(i => normalizeObject(i))
-    } else {
-      infoArray = [ normalizeObject(info) ]
-    }
-    console.log('Will download and upload %d videos.\n', infoArray.length)
-
-    for (const info of infoArray) {
-      await processVideo(info, program['language'])
-    }
-
-    // https://www.youtube.com/watch?v=2Upx39TBc1s
-    console.log('I\'m finished!')
-    process.exit(0)
-  })
-}
-
-function processVideo (info: any, languageCode: string) {
-  return new Promise(async res => {
-    if (program['verbose']) console.log('Fetching object.', info)
-
-    const videoInfo = await fetchObject(info)
-    if (program['verbose']) console.log('Fetched object.', videoInfo)
-
-    const result = await searchVideoWithSort(program['url'], videoInfo.title, '-match')
-
-    console.log('############################################################\n')
-
-    if (result.body.data.find(v => v.name === videoInfo.title)) {
-      console.log('Video "%s" already exists, don\'t reupload it.\n', videoInfo.title)
-      return res()
-    }
-
-    const path = join(__dirname, new Date().getTime() + '.mp4')
-
-    console.log('Downloading video "%s"...', videoInfo.title)
-
-    const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ]
-    try {
-      youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => {
-        if (err) {
-          console.error(err)
-          return res()
-        }
-
-        console.log(output.join('\n'))
-        await uploadVideoOnPeerTube(normalizeObject(videoInfo), path, languageCode)
-        return res()
-      })
-    } catch (err) {
-      console.log(err.message)
-      return res()
-    }
-  })
-}
-
-async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, language?: string) {
-  const category = await getCategory(videoInfo.categories)
-  const licence = getLicence(videoInfo.license)
-  let tags = []
-  if (Array.isArray(videoInfo.tags)) {
-    tags = videoInfo.tags
-      .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min)
-      .map(t => t.normalize())
-      .slice(0, 5)
-  }
-
-  let thumbnailfile
-  if (videoInfo.thumbnail) {
-    thumbnailfile = join(__dirname, 'thumbnail.jpg')
-
-    await doRequestAndSaveToFile({
-      method: 'GET',
-      uri: videoInfo.thumbnail
-    }, thumbnailfile)
-  }
-
-  const videoAttributes = {
-    name: truncate(videoInfo.title, {
-      'length': CONSTRAINTS_FIELDS.VIDEOS.NAME.max,
-      'separator': /,? +/,
-      'omission': ' […]'
-    }),
-    category,
-    licence,
-    language,
-    nsfw: isNSFW(videoInfo),
-    waitTranscoding: true,
-    commentsEnabled: true,
-    description: videoInfo.description || undefined,
-    support: undefined,
-    tags,
-    privacy: VideoPrivacy.PUBLIC,
-    fixture: videoPath,
-    thumbnailfile,
-    previewfile: thumbnailfile
-  }
-
-  console.log('\nUploading on PeerTube video "%s".', videoAttributes.name)
-  try {
-    await uploadVideo(program['url'], accessToken, videoAttributes)
-  } catch (err) {
-    if (err.message.indexOf('401') !== -1) {
-      console.log('Got 401 Unauthorized, token may have expired, renewing token and retry.')
-
-      const res = await login(program['url'], client, user)
-      accessToken = res.body.access_token
-
-      await uploadVideo(program['url'], accessToken, videoAttributes)
-    } else {
-      console.log(err.message)
-      process.exit(1)
-    }
-  }
-
-  await remove(videoPath)
-  if (thumbnailfile) await remove(thumbnailfile)
-
-  console.log('Uploaded video "%s"!\n', videoAttributes.name)
-}
-
-async function getCategory (categories: string[]) {
-  if (!categories) return undefined
-
-  const categoryString = categories[0]
-
-  if (categoryString === 'News & Politics') return 11
-
-  const res = await getVideoCategories(program['url'])
-  const categoriesServer = res.body
-
-  for (const key of Object.keys(categoriesServer)) {
-    const categoryServer = categoriesServer[key]
-    if (categoryString.toLowerCase() === categoryServer.toLowerCase()) return parseInt(key, 10)
-  }
-
-  return undefined
-}
-
-function getLicence (licence: string) {
-  if (!licence) return undefined
-
-  if (licence.indexOf('Creative Commons Attribution licence') !== -1) return 1
-
-  return undefined
-}
-
-function normalizeObject (obj: any) {
-  const newObj: any = {}
-
-  for (const key of Object.keys(obj)) {
-    // Deprecated key
-    if (key === 'resolution') continue
-
-    const value = obj[key]
-
-    if (typeof value === 'string') {
-      newObj[key] = value.normalize()
-    } else {
-      newObj[key] = value
-    }
-  }
-
-  return newObj
-}
-
-function fetchObject (info: any) {
-  const url = buildUrl(info)
-
-  return new Promise<any>(async (res, rej) => {
-    youtubeDL.getInfo(url, undefined, processOptions, async (err, videoInfo) => {
-      if (err) return rej(err)
-
-      const videoInfoWithUrl = Object.assign(videoInfo, { url })
-      return res(normalizeObject(videoInfoWithUrl))
-    })
-  })
-}
-
-function buildUrl (info: any) {
-  const webpageUrl = info.webpage_url as string
-  if (webpageUrl && webpageUrl.match(/^https?:\/\//)) return webpageUrl
-
-  const url = info.url as string
-  if (url && url.match(/^https?:\/\//)) return url
-
-  // It seems youtube-dl does not return the video url
-  return 'https://www.youtube.com/watch?v=' + info.id
-}
-
-function isNSFW (info: any) {
-  if (info.age_limit && info.age_limit >= 16) return true
-
-  return false
-}
diff --git a/server/tools/peertube-auth.ts b/server/tools/peertube-auth.ts
new file mode 100644 (file)
index 0000000..3343881
--- /dev/null
@@ -0,0 +1,140 @@
+import * as program from 'commander'
+import * as prompt from 'prompt'
+const Table = require('cli-table')
+import { getSettings, writeSettings, netrc } from './cli'
+import { isHostValid } from '../helpers/custom-validators/servers'
+import { isUserUsernameValid } from '../helpers/custom-validators/users'
+
+function delInstance (url: string) {
+  return new Promise((res, rej): void => {
+    getSettings()
+      .then(async (settings) => {
+        settings.remotes.splice(settings.remotes.indexOf(url))
+        await writeSettings(settings)
+        delete netrc.machines[url]
+        netrc.save()
+        res()
+      })
+      .catch(err => rej(err))
+  })
+}
+
+async function setInstance (url: string, username: string, password: string) {
+  return new Promise((res, rej): void => {
+    getSettings()
+      .then(async settings => {
+        if (settings.remotes.indexOf(url) === -1) {
+          settings.remotes.push(url)
+        }
+        await writeSettings(settings)
+        netrc.machines[url] = { login: username, password }
+        netrc.save()
+        res()
+      })
+      .catch(err => rej(err))
+  })
+}
+
+function isURLaPeerTubeInstance (url: string) {
+  return isHostValid(url) || (url.includes('localhost'))
+}
+
+program
+  .name('auth')
+  .usage('[command] [options]')
+
+program
+  .command('add')
+  .description('remember your accounts on remote instances for easier use')
+  .option('-u, --url <url>', 'Server url')
+  .option('-U, --username <username>', 'Username')
+  .option('-p, --password <token>', 'Password')
+  .option('--default', 'add the entry as the new default')
+  .action(options => {
+    prompt.override = options
+    prompt.start()
+    prompt.get({
+      properties: {
+        url: {
+          description: 'instance url',
+          conform: (value) => isURLaPeerTubeInstance(value),
+          required: true
+        },
+        username: {
+          conform: (value) => isUserUsernameValid(value),
+          message: 'Name must be only letters, spaces, or dashes',
+          required: true
+        },
+        password: {
+          hidden: true,
+          replace: '*',
+          required: true
+        }
+      }
+    }, (_, result) => {
+      setInstance(result.url, result.username, result.password)
+    })
+  })
+
+program
+  .command('del <url>')
+  .description('unregisters a remote instance')
+  .action((url) => {
+    delInstance(url)
+  })
+
+program
+  .command('list')
+  .description('lists registered remote instances')
+  .action(() => {
+    getSettings()
+      .then(settings => {
+        const table = new Table({
+          head: ['instance', 'login'],
+          colWidths: [30, 30]
+        })
+        netrc.loadSync()
+        settings.remotes.forEach(element => {
+          table.push([
+            element,
+            netrc.machines[element].login
+          ])
+        })
+
+        console.log(table.toString())
+      })
+  })
+
+program
+  .command('set-default <url>')
+  .description('set an existing entry as default')
+  .action((url) => {
+    getSettings()
+      .then(settings => {
+        const instanceExists = settings.remotes.indexOf(url) !== -1
+
+        if (instanceExists) {
+          settings.default = settings.remotes.indexOf(url)
+          writeSettings(settings)
+        } else {
+          console.log('<url> is not a registered instance.')
+          process.exit(-1)
+        }
+      })
+  })
+
+program.on('--help', function () {
+  console.log('  Examples:')
+  console.log()
+  console.log('    $ peertube add -u peertube.cpy.re -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"')
+  console.log('    $ peertube add -u peertube.cpy.re -U root')
+  console.log('    $ peertube list')
+  console.log('    $ peertube del peertube.cpy.re')
+  console.log()
+})
+
+if (!process.argv.slice(2).length) {
+  program.outputHelp()
+}
+
+program.parse(process.argv)
diff --git a/server/tools/peertube-get-access-token.ts b/server/tools/peertube-get-access-token.ts
new file mode 100644 (file)
index 0000000..eb2571a
--- /dev/null
@@ -0,0 +1,51 @@
+import * as program from 'commander'
+
+import {
+  getClient,
+  serverLogin,
+  Server,
+  Client,
+  User
+} from '../tests/utils/index'
+
+program
+  .option('-u, --url <url>', 'Server url')
+  .option('-n, --username <username>', 'Username')
+  .option('-p, --password <token>', 'Password')
+  .parse(process.argv)
+
+if (
+  !program['url'] ||
+  !program['username'] ||
+  !program['password']
+) {
+  if (!program['url']) console.error('--url field is required.')
+  if (!program['username']) console.error('--username field is required.')
+  if (!program['password']) console.error('--password field is required.')
+  process.exit(-1)
+}
+
+getClient(program.url)
+  .then(res => {
+    const server = {
+      url: program['url'],
+      user: {
+        username: program['username'],
+        password: program['password']
+      } as User,
+      client: {
+        id: res.body.client_id as string,
+        secret: res.body.client_secret as string
+      } as Client
+    } as Server
+
+    return serverLogin(server)
+  })
+  .then(accessToken => {
+    console.log(accessToken)
+    process.exit(0)
+  })
+  .catch(err => {
+    console.error(err)
+    process.exit(-1)
+  })
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts
new file mode 100644 (file)
index 0000000..13090a0
--- /dev/null
@@ -0,0 +1,323 @@
+// FIXME: https://github.com/nodejs/node/pull/16853
+require('tls').DEFAULT_ECDH_CURVE = 'auto'
+
+import * as program from 'commander'
+import { join } from 'path'
+import { VideoPrivacy } from '../../shared/models/videos'
+import { doRequestAndSaveToFile } from '../helpers/requests'
+import { CONSTRAINTS_FIELDS } from '../initializers'
+import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../tests/utils'
+import { truncate } from 'lodash'
+import * as prompt from 'prompt'
+import { remove } from 'fs-extra'
+import { safeGetYoutubeDL } from '../helpers/youtube-dl'
+import { getSettings, netrc } from './cli'
+
+let accessToken: string
+let client: { id: string, secret: string }
+
+const processOptions = {
+  cwd: __dirname,
+  maxBuffer: Infinity
+}
+
+program
+  .name('import-videos')
+  .option('-u, --url <url>', 'Server url')
+  .option('-U, --username <username>', 'Username')
+  .option('-p, --password <token>', 'Password')
+  .option('-t, --target-url <targetUrl>', 'Video target URL')
+  .option('-l, --language <languageCode>', 'Language ISO 639 code (fr or en...)')
+  .option('-v, --verbose', 'Verbose mode')
+  .parse(process.argv)
+
+getSettings()
+.then(settings => {
+  if (
+    (!program['url'] ||
+    !program['username'] ||
+    !program['password']) &&
+    (settings.remotes.length === 0)
+  ) {
+    if (!program['url']) console.error('--url field is required.')
+    if (!program['username']) console.error('--username field is required.')
+    if (!program['password']) console.error('--password field is required.')
+    if (!program['targetUrl']) console.error('--targetUrl field is required.')
+    process.exit(-1)
+  }
+
+  if (
+    (!program['url'] ||
+    !program['username'] ||
+    !program['password']) &&
+    (settings.remotes.length > 0)
+  ) {
+    if (!program['url']) {
+      program['url'] = (settings.default !== -1) ?
+        settings.remotes[settings.default] :
+        settings.remotes[0]
+    }
+    if (!program['username']) program['username'] = netrc.machines[program['url']].login
+    if (!program['password']) program['password'] = netrc.machines[program['url']].password
+  }
+
+  if (
+    !program['targetUrl']
+  ) {
+    if (!program['targetUrl']) console.error('--targetUrl field is required.')
+    process.exit(-1)
+  }
+
+  const user = {
+    username: program['username'],
+    password: program['password']
+  }
+
+  run(user, program['url']).catch(err => console.error(err))
+})
+
+async function promptPassword () {
+  return new Promise((res, rej) => {
+    prompt.start()
+    const schema = {
+      properties: {
+        password: {
+          hidden: true,
+          required: true
+        }
+      }
+    }
+    prompt.get(schema, function (err, result) {
+      if (err) {
+        return rej(err)
+      }
+      return res(result.password)
+    })
+  })
+}
+
+async function run (user, url: string) {
+  if (!user.password) {
+    user.password = await promptPassword()
+  }
+
+  const res = await getClient(url)
+  client = {
+    id: res.body.client_id,
+    secret: res.body.client_secret
+  }
+
+  const res2 = await login(url, client, user)
+  accessToken = res2.body.access_token
+
+  const youtubeDL = await safeGetYoutubeDL()
+
+  const options = [ '-j', '--flat-playlist', '--playlist-reverse' ]
+  youtubeDL.getInfo(program['targetUrl'], options, processOptions, async (err, info) => {
+    if (err) {
+      console.log(err.message)
+      process.exit(1)
+    }
+
+    let infoArray: any[]
+
+    // Normalize utf8 fields
+    if (Array.isArray(info) === true) {
+      infoArray = info.map(i => normalizeObject(i))
+    } else {
+      infoArray = [ normalizeObject(info) ]
+    }
+    console.log('Will download and upload %d videos.\n', infoArray.length)
+
+    for (const info of infoArray) {
+      await processVideo(info, program['language'], processOptions.cwd, url, user)
+    }
+
+    // https://www.youtube.com/watch?v=2Upx39TBc1s
+    console.log('I\'m finished!')
+    process.exit(0)
+  })
+}
+
+function processVideo (info: any, languageCode: string, cwd: string, url: string, user) {
+  return new Promise(async res => {
+    if (program['verbose']) console.log('Fetching object.', info)
+
+    const videoInfo = await fetchObject(info)
+    if (program['verbose']) console.log('Fetched object.', videoInfo)
+
+    const result = await searchVideoWithSort(url, videoInfo.title, '-match')
+
+    console.log('############################################################\n')
+
+    if (result.body.data.find(v => v.name === videoInfo.title)) {
+      console.log('Video "%s" already exists, don\'t reupload it.\n', videoInfo.title)
+      return res()
+    }
+
+    const path = join(cwd, new Date().getTime() + '.mp4')
+
+    console.log('Downloading video "%s"...', videoInfo.title)
+
+    const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ]
+    try {
+      const youtubeDL = await safeGetYoutubeDL()
+      youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => {
+        if (err) {
+          console.error(err)
+          return res()
+        }
+
+        console.log(output.join('\n'))
+        await uploadVideoOnPeerTube(normalizeObject(videoInfo), path, cwd, url, user, languageCode)
+        return res()
+      })
+    } catch (err) {
+      console.log(err.message)
+      return res()
+    }
+  })
+}
+
+async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, cwd: string, url: string, user, language?: string) {
+  const category = await getCategory(videoInfo.categories, url)
+  const licence = getLicence(videoInfo.license)
+  let tags = []
+  if (Array.isArray(videoInfo.tags)) {
+    tags = videoInfo.tags
+      .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min)
+      .map(t => t.normalize())
+      .slice(0, 5)
+  }
+
+  let thumbnailfile
+  if (videoInfo.thumbnail) {
+    thumbnailfile = join(cwd, 'thumbnail.jpg')
+
+    await doRequestAndSaveToFile({
+      method: 'GET',
+      uri: videoInfo.thumbnail
+    }, thumbnailfile)
+  }
+
+  const videoAttributes = {
+    name: truncate(videoInfo.title, {
+      'length': CONSTRAINTS_FIELDS.VIDEOS.NAME.max,
+      'separator': /,? +/,
+      'omission': ' […]'
+    }),
+    category,
+    licence,
+    language,
+    nsfw: isNSFW(videoInfo),
+    waitTranscoding: true,
+    commentsEnabled: true,
+    description: videoInfo.description || undefined,
+    support: undefined,
+    tags,
+    privacy: VideoPrivacy.PUBLIC,
+    fixture: videoPath,
+    thumbnailfile,
+    previewfile: thumbnailfile
+  }
+
+  console.log('\nUploading on PeerTube video "%s".', videoAttributes.name)
+  try {
+    await uploadVideo(url, accessToken, videoAttributes)
+  } catch (err) {
+    if (err.message.indexOf('401') !== -1) {
+      console.log('Got 401 Unauthorized, token may have expired, renewing token and retry.')
+
+      const res = await login(url, client, user)
+      accessToken = res.body.access_token
+
+      await uploadVideo(url, accessToken, videoAttributes)
+    } else {
+      console.log(err.message)
+      process.exit(1)
+    }
+  }
+
+  await remove(videoPath)
+  if (thumbnailfile) await remove(thumbnailfile)
+
+  console.log('Uploaded video "%s"!\n', videoAttributes.name)
+}
+
+async function getCategory (categories: string[], url: string) {
+  if (!categories) return undefined
+
+  const categoryString = categories[0]
+
+  if (categoryString === 'News & Politics') return 11
+
+  const res = await getVideoCategories(url)
+  const categoriesServer = res.body
+
+  for (const key of Object.keys(categoriesServer)) {
+    const categoryServer = categoriesServer[key]
+    if (categoryString.toLowerCase() === categoryServer.toLowerCase()) return parseInt(key, 10)
+  }
+
+  return undefined
+}
+
+/* ---------------------------------------------------------- */
+
+function getLicence (licence: string) {
+  if (!licence) return undefined
+
+  if (licence.indexOf('Creative Commons Attribution licence') !== -1) return 1
+
+  return undefined
+}
+
+function normalizeObject (obj: any) {
+  const newObj: any = {}
+
+  for (const key of Object.keys(obj)) {
+    // Deprecated key
+    if (key === 'resolution') continue
+
+    const value = obj[key]
+
+    if (typeof value === 'string') {
+      newObj[key] = value.normalize()
+    } else {
+      newObj[key] = value
+    }
+  }
+
+  return newObj
+}
+
+function fetchObject (info: any) {
+  const url = buildUrl(info)
+
+  return new Promise<any>(async (res, rej) => {
+    const youtubeDL = await safeGetYoutubeDL()
+    youtubeDL.getInfo(url, undefined, processOptions, async (err, videoInfo) => {
+      if (err) return rej(err)
+
+      const videoInfoWithUrl = Object.assign(videoInfo, { url })
+      return res(normalizeObject(videoInfoWithUrl))
+    })
+  })
+}
+
+function buildUrl (info: any) {
+  const webpageUrl = info.webpage_url as string
+  if (webpageUrl && webpageUrl.match(/^https?:\/\//)) return webpageUrl
+
+  const url = info.url as string
+  if (url && url.match(/^https?:\/\//)) return url
+
+  // It seems youtube-dl does not return the video url
+  return 'https://www.youtube.com/watch?v=' + info.id
+}
+
+function isNSFW (info: any) {
+  if (info.age_limit && info.age_limit >= 16) return true
+
+  return false
+}
diff --git a/server/tools/peertube-upload.ts b/server/tools/peertube-upload.ts
new file mode 100644 (file)
index 0000000..1f871e6
--- /dev/null
@@ -0,0 +1,127 @@
+import * as program from 'commander'
+import { access, constants } from 'fs-extra'
+import { isAbsolute } from 'path'
+import { getClient, login } from '../tests/utils'
+import { uploadVideo } from '../tests/utils/index'
+import { VideoPrivacy } from '../../shared/models/videos'
+import { netrc, getSettings } from './cli'
+
+program
+  .name('upload')
+  .option('-u, --url <url>', 'Server url')
+  .option('-U, --username <username>', 'Username')
+  .option('-p, --password <token>', 'Password')
+  .option('-n, --video-name <name>', 'Video name')
+  .option('-P, --privacy <privacy_number>', 'Privacy')
+  .option('-N, --nsfw', 'Video is Not Safe For Work')
+  .option('-c, --category <category_number>', 'Category number')
+  .option('-m, --comments-enabled', 'Enable comments')
+  .option('-l, --licence <licence_number>', 'Licence number')
+  .option('-L, --language <language_code>', 'Language ISO 639 code (fr or en...)')
+  .option('-d, --video-description <description>', 'Video description')
+  .option('-t, --tags <tags>', 'Video tags', list)
+  .option('-b, --thumbnail <thumbnailPath>', 'Thumbnail path')
+  .option('-v, --preview <previewPath>', 'Preview path')
+  .option('-f, --file <file>', 'Video absolute file path')
+  .parse(process.argv)
+
+if (!program['tags']) program['tags'] = []
+if (!program['nsfw']) program['nsfw'] = false
+if (!program['privacy']) program['privacy'] = VideoPrivacy.PUBLIC
+if (!program['commentsEnabled']) program['commentsEnabled'] = false
+
+getSettings()
+  .then(settings => {
+    if (
+      (!program['url'] ||
+      !program['username'] ||
+      !program['password']) &&
+      (settings.remotes.length === 0)
+    ) {
+      if (!program['url']) console.error('--url field is required.')
+      if (!program['username']) console.error('--username field is required.')
+      if (!program['password']) console.error('--password field is required.')
+      if (!program['videoName']) console.error('--video-name field is required.')
+      if (!program['file']) console.error('--file field is required.')
+      process.exit(-1)
+    }
+
+    if (
+      (!program['url'] ||
+      !program['username'] ||
+      !program['password']) &&
+      (settings.remotes.length > 0)
+    ) {
+      if (!program['url']) {
+        program['url'] = (settings.default !== -1) ?
+          settings.remotes[settings.default] :
+          settings.remotes[0]
+      }
+      if (!program['username']) program['username'] = netrc.machines[program['url']].login
+      if (!program['password']) program['password'] = netrc.machines[program['url']].password
+    }
+
+    if (
+      !program['videoName'] ||
+      !program['file']
+    ) {
+      if (!program['videoName']) console.error('--video-name field is required.')
+      if (!program['file']) console.error('--file field is required.')
+      process.exit(-1)
+    }
+
+    if (isAbsolute(program['file']) === false) {
+      console.error('File path should be absolute.')
+      process.exit(-1)
+    }
+
+    run().catch(err => console.error(err))
+  })
+
+async function run () {
+  const res = await getClient(program[ 'url' ])
+  const client = {
+    id: res.body.client_id,
+    secret: res.body.client_secret
+  }
+
+  const user = {
+    username: program[ 'username' ],
+    password: program[ 'password' ]
+  }
+
+  const res2 = await login(program[ 'url' ], client, user)
+  const accessToken = res2.body.access_token
+
+  await access(program[ 'file' ], constants.F_OK)
+
+  console.log('Uploading %s video...', program[ 'videoName' ])
+
+  const videoAttributes = {
+    name: program['videoName'],
+    category: program['category'],
+    licence: program['licence'],
+    language: program['language'],
+    nsfw: program['nsfw'],
+    description: program['videoDescription'],
+    tags: program['tags'],
+    commentsEnabled: program['commentsEnabled'],
+    fixture: program['file'],
+    thumbnailfile: program['thumbnail'],
+    previewfile: program['preview'],
+    waitTranscoding: true,
+    privacy: program['privacy'],
+    support: undefined
+  }
+
+  await uploadVideo(program['url'], accessToken, videoAttributes)
+
+  console.log(`Video ${program['videoName']} uploaded.`)
+  process.exit(0)
+}
+
+// ----------------------------------------------------------------------------
+
+function list (val) {
+  return val.split(',')
+}
diff --git a/server/tools/peertube-watch.ts b/server/tools/peertube-watch.ts
new file mode 100644 (file)
index 0000000..bf7274a
--- /dev/null
@@ -0,0 +1,61 @@
+import * as program from 'commander'
+import * as summon from 'summon-install'
+import { join } from 'path'
+import { execSync } from 'child_process'
+import { root } from '../helpers/core-utils'
+
+let videoURL
+
+program
+  .name('watch')
+  .arguments('<url>')
+  .option('-g, --gui <player>', 'player type', /^(airplay|stdout|chromecast|mpv|vlc|mplayer|ascii|xbmc)$/i, 'ascii')
+  .option('-i, --invert', 'invert colors (ascii player only)', true)
+  .option('-r, --resolution <res>', 'video resolution', /^(240|360|720|1080)$/i, '720')
+  .on('--help', function () {
+    console.log('  Available Players:')
+    console.log()
+    console.log('    - ascii')
+    console.log('    - mpv')
+    console.log('    - mplayer')
+    console.log('    - vlc')
+    console.log('    - stdout')
+    console.log('    - xbmc')
+    console.log('    - airplay')
+    console.log('    - chromecast')
+    console.log()
+    console.log('  Note: \'ascii\' is the only option not using WebTorrent and not seeding back the video.')
+    console.log()
+    console.log('  Examples:')
+    console.log()
+    console.log('    $ peertube watch -g mpv https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10')
+    console.log('    $ peertube watch --gui stdout https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10')
+    console.log('    $ peertube watch https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10')
+    console.log()
+  })
+  .action((url) => {
+    videoURL = url
+  })
+  .parse(process.argv)
+
+if (!videoURL) {
+  console.error('<url> positional argument is required.')
+  process.exit(-1)
+} else { program['url'] = videoURL }
+
+handler(program)
+
+function handler (argv) {
+  if (argv['gui'] === 'ascii') {
+    summon('peerterminal')
+    const peerterminal = summon('peerterminal')
+    peerterminal([ '--link', videoURL, '--invert', argv['invert'] ])
+  } else {
+    summon('webtorrent-hybrid')
+    const CMD = 'node ' + join(root(), 'node_modules', 'webtorrent-hybrid', 'bin', 'cmd.js')
+    const CMDargs = ` --${argv.gui} ` +
+                    argv['url'].replace('videos/watch', 'download/torrents') +
+                    `-${argv.resolution}.torrent`
+    execSync(CMD + CMDargs)
+  }
+}
diff --git a/server/tools/peertube.ts b/server/tools/peertube.ts
new file mode 100755 (executable)
index 0000000..7441161
--- /dev/null
@@ -0,0 +1,81 @@
+#!/usr/bin/env node
+
+import * as program from 'commander'
+import {
+  version,
+  getSettings
+} from './cli'
+
+program
+  .version(version(), '-v, --version')
+  .usage('[command] [options]')
+
+/* Subcommands automatically loaded in the directory and beginning by peertube-* */
+program
+  .command('auth [action]', 'register your accounts on remote instances to use them with other commands')
+  .command('upload', 'upload a video').alias('up')
+  .command('import-videos', 'import a video from a streaming platform').alias('import')
+  .command('get-access-token', 'get a peertube access token', { noHelp: true }).alias('token')
+  .command('watch', 'watch a video in the terminal ✩°。⋆').alias('w')
+
+/* Not Yet Implemented */
+program
+  .command('plugins [action]',
+           'manage plugins on a local instance',
+           { noHelp: true } as program.CommandOptions
+          ).alias('p')
+  .command('diagnostic [action]',
+           'like couple therapy, but for your instance',
+           { noHelp: true } as program.CommandOptions
+          ).alias('d')
+  .command('admin',
+           'manage an instance where you have elevated rights',
+          { noHelp: true } as program.CommandOptions
+          ).alias('a')
+
+// help on no command
+if (!process.argv.slice(2).length) {
+  const logo = '░P░e░e░r░T░u░b░e░'
+  console.log(`
+  ___/),.._                           ` + logo + `
+/'   ,.   ."'._
+(     "'   '-.__"-._             ,-
+\\'='='),  "\\ -._-"-.          -"/
+      / ""/"\\,_\\,__""       _" /,-
+     /   /                -" _/"/
+    /   |    ._\\\\ |\\  |_.".-"  /
+   /    |   __\\)|)|),/|_." _,."
+  /     \_."   " ") | ).-""---''--
+ (                  "/.""7__-""''
+ |                   " ."._--._
+ \\       \\ (_    __   ""   ".,_
+  \\.,.    \\  ""   -"".-"
+   ".,_,  (",_-,,,-".-
+       "'-,\\_   __,-"
+             ",)" ")
+              /"\\-"
+            ,"\\/
+      _,.__/"\\/_                     (the CLI for red chocobos)
+     / \\) "./,  ".
+  --/---"---" "-) )---- by Chocobozzz et al.`)
+}
+
+getSettings()
+  .then(settings => {
+    const state = (settings.default === -1) ?
+      'no instance selected, commands will require explicit arguments' :
+      ('instance ' + settings.remotes[settings.default] + ' selected')
+    program
+      .on('--help', function () {
+        console.log()
+        console.log('  State: ' + state)
+        console.log()
+        console.log('  Examples:')
+        console.log()
+        console.log('    $ peertube auth add -u "PEERTUBE_URL" -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"')
+        console.log('    $ peertube up <videoFile>')
+        console.log('    $ peertube watch https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10')
+        console.log()
+      })
+      .parse(process.argv)
+  })
diff --git a/server/tools/upload.ts b/server/tools/upload.ts
deleted file mode 100644 (file)
index 9b104d3..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-import * as program from 'commander'
-import { access, constants } from 'fs-extra'
-import { isAbsolute } from 'path'
-import { getClient, login } from '../tests/utils'
-import { uploadVideo } from '../tests/utils/index'
-import { VideoPrivacy } from '../../shared/models/videos'
-
-program
-  .option('-u, --url <url>', 'Server url')
-  .option('-U, --username <username>', 'Username')
-  .option('-p, --password <token>', 'Password')
-  .option('-n, --video-name <name>', 'Video name')
-  .option('-P, --privacy <privacy number>', 'Privacy')
-  .option('-N, --nsfw', 'Video is Not Safe For Work')
-  .option('-c, --category <category number>', 'Category number')
-  .option('-m, --comments-enabled', 'Enable comments')
-  .option('-l, --licence <licence number>', 'Licence number')
-  .option('-L, --language <language code>', 'Language ISO 639 code (fr or en...)')
-  .option('-d, --video-description <description>', 'Video description')
-  .option('-t, --tags <tags>', 'Video tags', list)
-  .option('-b, --thumbnail <thumbnailPath>', 'Thumbnail path')
-  .option('-v, --preview <previewPath>', 'Preview path')
-  .option('-f, --file <file>', 'Video absolute file path')
-  .parse(process.argv)
-
-if (!program['tags']) program['tags'] = []
-if (!program['nsfw']) program['nsfw'] = false
-if (!program['privacy']) program['privacy'] = VideoPrivacy.PUBLIC
-if (!program['commentsEnabled']) program['commentsEnabled'] = false
-
-if (
-  !program['url'] ||
-  !program['username'] ||
-  !program['password'] ||
-  !program['videoName'] ||
-  !program['file']
-) {
-  if (!program['url']) console.error('--url field is required.')
-  if (!program['username']) console.error('--username field is required.')
-  if (!program['password']) console.error('--password field is required.')
-  if (!program['videoName']) console.error('--video-name field is required.')
-  if (!program['file']) console.error('--file field is required.')
-  process.exit(-1)
-}
-
-if (isAbsolute(program['file']) === false) {
-  console.error('File path should be absolute.')
-  process.exit(-1)
-}
-
-run().catch(err => console.error(err))
-
-async function run () {
-  const res = await getClient(program[ 'url' ])
-  const client = {
-    id: res.body.client_id,
-    secret: res.body.client_secret
-  }
-
-  const user = {
-    username: program[ 'username' ],
-    password: program[ 'password' ]
-  }
-
-  const res2 = await login(program[ 'url' ], client, user)
-  const accessToken = res2.body.access_token
-
-  await access(program[ 'file' ], constants.F_OK)
-
-  console.log('Uploading %s video...', program[ 'videoName' ])
-
-  const videoAttributes = {
-    name: program['videoName'],
-    category: program['category'],
-    licence: program['licence'],
-    language: program['language'],
-    nsfw: program['nsfw'],
-    description: program['videoDescription'],
-    tags: program['tags'],
-    commentsEnabled: program['commentsEnabled'],
-    fixture: program['file'],
-    thumbnailfile: program['thumbnail'],
-    previewfile: program['preview'],
-    waitTranscoding: true,
-    privacy: program['privacy'],
-    support: undefined
-  }
-
-  await uploadVideo(program['url'], accessToken, videoAttributes)
-
-  console.log(`Video ${program['videoName']} uploaded.`)
-  process.exit(0)
-}
-
-// ----------------------------------------------------------------------------
-
-function list (val) {
-  return val.split(',')
-}
index 0a2f1f11b35f98d99757b86c85c9b8a6dfaebe13..1db29edc0d9e3c3b84b130bba0076a6856241f32 100644 (file)
@@ -1,14 +1,60 @@
 # CLI tools guide
-
+ - [CLI wrapper](#cli-wrapper)
  - [Remote tools](#remote-tools)
-   - [import-videos.js](#import-videosjs)
-   - [upload.js](#uploadjs)
+   - [peertube-import-videos.js](#peertube-import-videosjs)
+   - [peertube-upload.js](#peertube-uploadjs)
+   - [peertube-watch.js](#peertube-watch)
  - [Server tools](#server-tools)
    - [parse-log](#parse-log)
    - [create-transcoding-job.js](#create-transcoding-jobjs)
    - [create-import-video-file-job.js](#create-import-video-file-jobjs)
    - [prune-storage.js](#prune-storagejs)
 
+## CLI wrapper
+
+The wrapper provides a convenient interface to most scripts, and requires the [same dependencies](#dependencies). You can access it as `peertube` via an alias in your `.bashrc` like `alias peertube="node ${PEERTUBE_PATH}/dist/server/tools/peertube.js"`:
+
+```
+  Usage: peertube [command] [options]
+
+  Options:
+
+    -v, --version         output the version number
+    -h, --help            output usage information
+
+  Commands:
+
+    auth [action]         register your accounts on remote instances to use them with other commands
+    upload|up             upload a video
+    import-videos|import  import a video from a streaming platform
+    watch|w               watch a video in the terminal ✩°。⋆
+    help [cmd]            display help for [cmd]
+```
+
+The wrapper can keep track of instances you have an account on. We limit to one account per instance for now.
+
+```bash
+$ peertube auth add -u "PEERTUBE_URL" -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"
+$ peertube auth list
+┌──────────────────────────────┬──────────────────────────────┐
+│ instance                     │ login                        │
+├──────────────────────────────┼──────────────────────────────┤
+│ "PEERTUBE_URL"               │ "PEERTUBE_USER"              │
+└──────────────────────────────┴──────────────────────────────┘
+```
+
+You can now use that account to upload videos without feeding the same parameters again.
+
+```bash
+$ peertube up <videoFile>
+```
+
+And now that your video is online, you can watch it from the confort of your terminal (use `peertube watch --help` to see the supported players):
+
+```bash
+$ peertube watch https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10
+```
+
 ## Remote Tools
 
 You need at least 512MB RAM to run the script.
@@ -40,13 +86,13 @@ $ cd ${CLONE}
 $ npm run build:server
 ```
 
-### import-videos.js
+### peertube-import-videos.js
 
 You can use this script to import videos from all [supported sites of youtube-dl](https://rg3.github.io/youtube-dl/supportedsites.html) into PeerTube.  
 Be sure you own the videos or have the author's authorization to do so.
 
 ```sh
-$ node dist/server/tools/import-videos.js \
+$ node dist/server/tools/peertube-import-videos.js \
     -u "PEERTUBE_URL" \
     -U "PEERTUBE_USER" \
     --password "PEERTUBE_PASSWORD" \
@@ -70,7 +116,7 @@ Already downloaded videos will not be uploaded twice, so you can run and re-run
 Videos will be publicly available after transcoding (you can see them before that in your account on the web interface).
 
 
-### upload.js
+### peertube-upload.js
 
 You can use this script to import videos directly from the CLI.
 
@@ -78,9 +124,24 @@ Videos will be publicly available after transcoding (you can see them before tha
 
 ```
 $ cd ${CLONE}
-$ node dist/server/tools/upload.js --help
+$ node dist/server/tools/peertube-upload.js --help
 ```
 
+### peertube-watch.js
+
+You can use this script to play videos directly from the CLI.
+
+It provides support for different players:
+
+- ascii (default ; plays in ascii art in your terminal!)
+- mpv
+- mplayer
+- vlc
+- stdout
+- xbmc
+- airplay
+- chromecast
+
 
 ## Server tools
 
index 7633465b2094db4b31274edfae04e633a6676009..c84b179cfe01cc2740a39dd61484067db1065db2 100644 (file)
@@ -6,6 +6,7 @@
     "sourceMap": false,
     "experimentalDecorators": true,
     "emitDecoratorMetadata": true,
+    "removeComments": true,
     "outDir": "./dist",
     "lib": [
       "dom",