Browse Source

one cli to unite them all

Ash nazg thrakatulûk agh burzum-ishi krimpatul

- refactor import-videos to use the youtubeDL helper
- add very basic tests for the cli
Rigel Kent 5 years ago
parent
commit
8704acf49e

+ 10 - 0
package.json

@@ -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",
@@ -113,6 +118,7 @@
     "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",
@@ -130,6 +136,7 @@
     "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",
@@ -196,5 +203,8 @@
   "scripty": {
     "silent": true
   },
+  "summon": {
+    "silent": true
+  },
   "sasslintConfig": "client/.sass-lint.yml"
 }

+ 12 - 11
server/helpers/youtube-dl.ts

@@ -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 = {}
 

+ 1 - 0
server/tests/cli/index.ts

@@ -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'

+ 51 - 0
server/tests/cli/peertube.ts

@@ -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 ])
+  })
+})

+ 63 - 0
server/tools/cli.ts

@@ -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
+}

+ 140 - 0
server/tools/peertube-auth.ts

@@ -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)

+ 4 - 1
server/tools/get-access-token.ts → server/tools/peertube-get-access-token.ts

@@ -19,7 +19,10 @@ if (
   !program['username'] ||
   !program['password']
 ) {
-  throw new Error('All arguments are required.')
+  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)

+ 73 - 36
server/tools/import-videos.ts → server/tools/peertube-import-videos.ts

@@ -3,7 +3,6 @@ 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'
@@ -11,8 +10,19 @@ import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo
 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')
@@ -21,29 +31,50 @@ program
   .option('-v, --verbose', 'Verbose mode')
   .parse(process.argv)
 
-if (
-  !program['url'] ||
-  !program['username'] ||
-  !program['targetUrl']
-) {
-  console.error('All arguments are required.')
-  process.exit(-1)
-}
+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)
+  }
 
-const user = {
-  username: program['username'],
-  password: program['password']
-}
+  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
+  }
 
-run().catch(err => console.error(err))
+  if (
+    !program['targetUrl']
+  ) {
+    if (!program['targetUrl']) console.error('--targetUrl field is required.')
+    process.exit(-1)
+  }
 
-let accessToken: string
-let client: { id: string, secret: string }
+  const user = {
+    username: program['username'],
+    password: program['password']
+  }
 
-const processOptions = {
-  cwd: __dirname,
-  maxBuffer: Infinity
-}
+  run(user, program['url']).catch(err => console.error(err))
+})
 
 async function promptPassword () {
   return new Promise((res, rej) => {
@@ -65,20 +96,22 @@ async function promptPassword () {
   })
 }
 
-async function run () {
+async function run (user, url: string) {
   if (!user.password) {
     user.password = await promptPassword()
   }
 
-  const res = await getClient(program['url'])
+  const res = await getClient(url)
   client = {
     id: res.body.client_id,
     secret: res.body.client_secret
   }
 
-  const res2 = await login(program['url'], client, user)
+  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) {
@@ -97,7 +130,7 @@ async function run () {
     console.log('Will download and upload %d videos.\n', infoArray.length)
 
     for (const info of infoArray) {
-      await processVideo(info, program['language'])
+      await processVideo(info, program['language'], processOptions.cwd, url, user)
     }
 
     // https://www.youtube.com/watch?v=2Upx39TBc1s
@@ -106,14 +139,14 @@ async function run () {
   })
 }
 
-function processVideo (info: any, languageCode: string) {
+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(program['url'], videoInfo.title, '-match')
+    const result = await searchVideoWithSort(url, videoInfo.title, '-match')
 
     console.log('############################################################\n')
 
@@ -122,12 +155,13 @@ function processVideo (info: any, languageCode: string) {
       return res()
     }
 
-    const path = join(__dirname, new Date().getTime() + '.mp4')
+    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)
@@ -135,7 +169,7 @@ function processVideo (info: any, languageCode: string) {
         }
 
         console.log(output.join('\n'))
-        await uploadVideoOnPeerTube(normalizeObject(videoInfo), path, languageCode)
+        await uploadVideoOnPeerTube(normalizeObject(videoInfo), path, cwd, url, user, languageCode)
         return res()
       })
     } catch (err) {
@@ -145,8 +179,8 @@ function processVideo (info: any, languageCode: string) {
   })
 }
 
-async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, language?: string) {
-  const category = await getCategory(videoInfo.categories)
+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)) {
@@ -158,7 +192,7 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, languag
 
   let thumbnailfile
   if (videoInfo.thumbnail) {
-    thumbnailfile = join(__dirname, 'thumbnail.jpg')
+    thumbnailfile = join(cwd, 'thumbnail.jpg')
 
     await doRequestAndSaveToFile({
       method: 'GET',
@@ -189,15 +223,15 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, languag
 
   console.log('\nUploading on PeerTube video "%s".', videoAttributes.name)
   try {
-    await uploadVideo(program['url'], accessToken, videoAttributes)
+    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(program['url'], client, user)
+      const res = await login(url, client, user)
       accessToken = res.body.access_token
 
-      await uploadVideo(program['url'], accessToken, videoAttributes)
+      await uploadVideo(url, accessToken, videoAttributes)
     } else {
       console.log(err.message)
       process.exit(1)
@@ -210,14 +244,14 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, languag
   console.log('Uploaded video "%s"!\n', videoAttributes.name)
 }
 
-async function getCategory (categories: string[]) {
+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(program['url'])
+  const res = await getVideoCategories(url)
   const categoriesServer = res.body
 
   for (const key of Object.keys(categoriesServer)) {
@@ -228,6 +262,8 @@ async function getCategory (categories: string[]) {
   return undefined
 }
 
+/* ---------------------------------------------------------- */
+
 function getLicence (licence: string) {
   if (!licence) return undefined
 
@@ -259,6 +295,7 @@ 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)
 

+ 51 - 23
server/tools/upload.ts → server/tools/peertube-upload.ts

@@ -4,18 +4,20 @@ 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('-P, --privacy <privacy_number>', 'Privacy')
   .option('-N, --nsfw', 'Video is Not Safe For Work')
-  .option('-c, --category <category number>', 'Category number')
+  .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('-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')
@@ -28,27 +30,53 @@ 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)
-}
+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 (isAbsolute(program['file']) === false) {
-  console.error('File path should be absolute.')
-  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))
+    run().catch(err => console.error(err))
+  })
 
 async function run () {
   const res = await getClient(program[ 'url' ])

+ 61 - 0
server/tools/peertube-watch.ts

@@ -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)
+  }
+}

+ 81 - 0
server/tools/peertube.ts

@@ -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)
+  })

+ 68 - 7
support/doc/tools.md

@@ -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
 

+ 1 - 0
tsconfig.json

@@ -6,6 +6,7 @@
     "sourceMap": false,
     "experimentalDecorators": true,
     "emitDecoratorMetadata": true,
+    "removeComments": true,
     "outDir": "./dist",
     "lib": [
       "dom",