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
pull/941/head
Rigel Kent 2018-09-13 14:27:44 +02:00
parent 1d9d9cfdcf
commit 8704acf49e
No known key found for this signature in database
GPG Key ID: EA12971B0E438F36
13 changed files with 622 additions and 84 deletions

View File

@ -7,6 +7,9 @@
"engines": { "engines": {
"node": ">=8.x" "node": ">=8.x"
}, },
"bin": {
"peertube": "dist/server/tools/peertube.js"
},
"author": { "author": {
"name": "Florian Bigard", "name": "Florian Bigard",
"email": "florian.bigard@gmail.com", "email": "florian.bigard@gmail.com",
@ -78,6 +81,7 @@
"@types/bluebird": "3.5.21" "@types/bluebird": "3.5.21"
}, },
"dependencies": { "dependencies": {
"application-config": "^1.0.1",
"async": "^2.0.0", "async": "^2.0.0",
"async-lock": "^1.1.2", "async-lock": "^1.1.2",
"async-lru": "^1.1.1", "async-lru": "^1.1.1",
@ -86,6 +90,7 @@
"bluebird": "^3.5.0", "bluebird": "^3.5.0",
"body-parser": "^1.12.4", "body-parser": "^1.12.4",
"bull": "^3.4.2", "bull": "^3.4.2",
"cli-table": "^0.3.1",
"bytes": "^3.0.0", "bytes": "^3.0.0",
"commander": "^2.13.0", "commander": "^2.13.0",
"concurrently": "^4.0.1", "concurrently": "^4.0.1",
@ -113,6 +118,7 @@
"magnet-uri": "^5.1.4", "magnet-uri": "^5.1.4",
"morgan": "^1.5.3", "morgan": "^1.5.3",
"multer": "^1.1.0", "multer": "^1.1.0",
"netrc-parser": "^3.1.6",
"nodemailer": "^4.4.2", "nodemailer": "^4.4.2",
"parse-torrent": "^6.0.0", "parse-torrent": "^6.0.0",
"password-generator": "^2.0.2", "password-generator": "^2.0.2",
@ -130,6 +136,7 @@
"sequelize-typescript": "0.6.6", "sequelize-typescript": "0.6.6",
"sharp": "^0.20.0", "sharp": "^0.20.0",
"srt-to-vtt": "^1.1.2", "srt-to-vtt": "^1.1.2",
"summon-install": "^0.4.3",
"useragent": "^2.3.0", "useragent": "^2.3.0",
"uuid": "^3.1.0", "uuid": "^3.1.0",
"validator": "^10.2.0", "validator": "^10.2.0",
@ -196,5 +203,8 @@
"scripty": { "scripty": {
"silent": true "silent": true
}, },
"summon": {
"silent": true
},
"sasslintConfig": "client/.sass-lint.yml" "sasslintConfig": "client/.sass-lint.yml"
} }

View File

@ -14,9 +14,9 @@ export type YoutubeDLInfo = {
thumbnailUrl?: string thumbnailUrl?: string
} }
function getYoutubeDLInfo (url: string): Promise<YoutubeDLInfo> { function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> {
return new Promise<YoutubeDLInfo>(async (res, rej) => { return new Promise<YoutubeDLInfo>(async (res, rej) => {
const options = [ '-j', '--flat-playlist' ] const options = opts || [ '-j', '--flat-playlist' ]
const youtubeDL = await safeGetYoutubeDL() const youtubeDL = await safeGetYoutubeDL()
youtubeDL.getInfo(url, options, (err, info) => { youtubeDL.getInfo(url, options, (err, info) => {
@ -48,15 +48,6 @@ function downloadYoutubeDLVideo (url: string) {
}) })
} }
// ---------------------------------------------------------------------------
export {
downloadYoutubeDLVideo,
getYoutubeDLInfo
}
// ---------------------------------------------------------------------------
async function safeGetYoutubeDL () { async function safeGetYoutubeDL () {
let youtubeDL let youtubeDL
@ -71,6 +62,16 @@ async function safeGetYoutubeDL () {
return youtubeDL return youtubeDL
} }
// ---------------------------------------------------------------------------
export {
downloadYoutubeDLVideo,
getYoutubeDLInfo,
safeGetYoutubeDL
}
// ---------------------------------------------------------------------------
function normalizeObject (obj: any) { function normalizeObject (obj: any) {
const newObj: any = {} const newObj: any = {}

View File

@ -1,5 +1,6 @@
// Order of the tests we want to execute // Order of the tests we want to execute
import './create-transcoding-job' import './create-transcoding-job'
import './create-import-video-file-job' import './create-import-video-file-job'
import './peertube'
import './reset-password' import './reset-password'
import './update-host' import './update-host'

View File

@ -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
server/tools/cli.ts Normal file
View File

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

View File

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

View File

@ -19,7 +19,10 @@ if (
!program['username'] || !program['username'] ||
!program['password'] !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) getClient(program.url)

View File

@ -3,7 +3,6 @@ require('tls').DEFAULT_ECDH_CURVE = 'auto'
import * as program from 'commander' import * as program from 'commander'
import { join } from 'path' import { join } from 'path'
import * as youtubeDL from 'youtube-dl'
import { VideoPrivacy } from '../../shared/models/videos' import { VideoPrivacy } from '../../shared/models/videos'
import { doRequestAndSaveToFile } from '../helpers/requests' import { doRequestAndSaveToFile } from '../helpers/requests'
import { CONSTRAINTS_FIELDS } from '../initializers' import { CONSTRAINTS_FIELDS } from '../initializers'
@ -11,31 +10,8 @@ import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo
import { truncate } from 'lodash' import { truncate } from 'lodash'
import * as prompt from 'prompt' import * as prompt from 'prompt'
import { remove } from 'fs-extra' import { remove } from 'fs-extra'
import { safeGetYoutubeDL } from '../helpers/youtube-dl'
program import { getSettings, netrc } from './cli'
.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 accessToken: string
let client: { id: string, secret: string } let client: { id: string, secret: string }
@ -45,6 +21,61 @@ const processOptions = {
maxBuffer: Infinity 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 () { async function promptPassword () {
return new Promise((res, rej) => { return new Promise((res, rej) => {
prompt.start() prompt.start()
@ -65,20 +96,22 @@ async function promptPassword () {
}) })
} }
async function run () { async function run (user, url: string) {
if (!user.password) { if (!user.password) {
user.password = await promptPassword() user.password = await promptPassword()
} }
const res = await getClient(program['url']) const res = await getClient(url)
client = { client = {
id: res.body.client_id, id: res.body.client_id,
secret: res.body.client_secret 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 accessToken = res2.body.access_token
const youtubeDL = await safeGetYoutubeDL()
const options = [ '-j', '--flat-playlist', '--playlist-reverse' ] const options = [ '-j', '--flat-playlist', '--playlist-reverse' ]
youtubeDL.getInfo(program['targetUrl'], options, processOptions, async (err, info) => { youtubeDL.getInfo(program['targetUrl'], options, processOptions, async (err, info) => {
if (err) { if (err) {
@ -97,7 +130,7 @@ async function run () {
console.log('Will download and upload %d videos.\n', infoArray.length) console.log('Will download and upload %d videos.\n', infoArray.length)
for (const info of infoArray) { 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 // 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 => { return new Promise(async res => {
if (program['verbose']) console.log('Fetching object.', info) if (program['verbose']) console.log('Fetching object.', info)
const videoInfo = await fetchObject(info) const videoInfo = await fetchObject(info)
if (program['verbose']) console.log('Fetched object.', videoInfo) 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') console.log('############################################################\n')
@ -122,12 +155,13 @@ function processVideo (info: any, languageCode: string) {
return res() 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) console.log('Downloading video "%s"...', videoInfo.title)
const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ] const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ]
try { try {
const youtubeDL = await safeGetYoutubeDL()
youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => { youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => {
if (err) { if (err) {
console.error(err) console.error(err)
@ -135,7 +169,7 @@ function processVideo (info: any, languageCode: string) {
} }
console.log(output.join('\n')) console.log(output.join('\n'))
await uploadVideoOnPeerTube(normalizeObject(videoInfo), path, languageCode) await uploadVideoOnPeerTube(normalizeObject(videoInfo), path, cwd, url, user, languageCode)
return res() return res()
}) })
} catch (err) { } catch (err) {
@ -145,8 +179,8 @@ function processVideo (info: any, languageCode: string) {
}) })
} }
async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, language?: string) { async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, cwd: string, url: string, user, language?: string) {
const category = await getCategory(videoInfo.categories) const category = await getCategory(videoInfo.categories, url)
const licence = getLicence(videoInfo.license) const licence = getLicence(videoInfo.license)
let tags = [] let tags = []
if (Array.isArray(videoInfo.tags)) { if (Array.isArray(videoInfo.tags)) {
@ -158,7 +192,7 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, languag
let thumbnailfile let thumbnailfile
if (videoInfo.thumbnail) { if (videoInfo.thumbnail) {
thumbnailfile = join(__dirname, 'thumbnail.jpg') thumbnailfile = join(cwd, 'thumbnail.jpg')
await doRequestAndSaveToFile({ await doRequestAndSaveToFile({
method: 'GET', method: 'GET',
@ -189,15 +223,15 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, languag
console.log('\nUploading on PeerTube video "%s".', videoAttributes.name) console.log('\nUploading on PeerTube video "%s".', videoAttributes.name)
try { try {
await uploadVideo(program['url'], accessToken, videoAttributes) await uploadVideo(url, accessToken, videoAttributes)
} catch (err) { } catch (err) {
if (err.message.indexOf('401') !== -1) { if (err.message.indexOf('401') !== -1) {
console.log('Got 401 Unauthorized, token may have expired, renewing token and retry.') 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 accessToken = res.body.access_token
await uploadVideo(program['url'], accessToken, videoAttributes) await uploadVideo(url, accessToken, videoAttributes)
} else { } else {
console.log(err.message) console.log(err.message)
process.exit(1) process.exit(1)
@ -210,14 +244,14 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, languag
console.log('Uploaded video "%s"!\n', videoAttributes.name) console.log('Uploaded video "%s"!\n', videoAttributes.name)
} }
async function getCategory (categories: string[]) { async function getCategory (categories: string[], url: string) {
if (!categories) return undefined if (!categories) return undefined
const categoryString = categories[0] const categoryString = categories[0]
if (categoryString === 'News & Politics') return 11 if (categoryString === 'News & Politics') return 11
const res = await getVideoCategories(program['url']) const res = await getVideoCategories(url)
const categoriesServer = res.body const categoriesServer = res.body
for (const key of Object.keys(categoriesServer)) { for (const key of Object.keys(categoriesServer)) {
@ -228,6 +262,8 @@ async function getCategory (categories: string[]) {
return undefined return undefined
} }
/* ---------------------------------------------------------- */
function getLicence (licence: string) { function getLicence (licence: string) {
if (!licence) return undefined if (!licence) return undefined
@ -259,6 +295,7 @@ function fetchObject (info: any) {
const url = buildUrl(info) const url = buildUrl(info)
return new Promise<any>(async (res, rej) => { return new Promise<any>(async (res, rej) => {
const youtubeDL = await safeGetYoutubeDL()
youtubeDL.getInfo(url, undefined, processOptions, async (err, videoInfo) => { youtubeDL.getInfo(url, undefined, processOptions, async (err, videoInfo) => {
if (err) return rej(err) if (err) return rej(err)

View File

@ -4,18 +4,20 @@ import { isAbsolute } from 'path'
import { getClient, login } from '../tests/utils' import { getClient, login } from '../tests/utils'
import { uploadVideo } from '../tests/utils/index' import { uploadVideo } from '../tests/utils/index'
import { VideoPrivacy } from '../../shared/models/videos' import { VideoPrivacy } from '../../shared/models/videos'
import { netrc, getSettings } from './cli'
program program
.name('upload')
.option('-u, --url <url>', 'Server url') .option('-u, --url <url>', 'Server url')
.option('-U, --username <username>', 'Username') .option('-U, --username <username>', 'Username')
.option('-p, --password <token>', 'Password') .option('-p, --password <token>', 'Password')
.option('-n, --video-name <name>', 'Video name') .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('-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('-m, --comments-enabled', 'Enable comments')
.option('-l, --licence <licence number>', 'Licence number') .option('-l, --licence <licence_number>', 'Licence number')
.option('-L, --language <language code>', 'Language ISO 639 code (fr or en...)') .option('-L, --language <language_code>', 'Language ISO 639 code (fr or en...)')
.option('-d, --video-description <description>', 'Video description') .option('-d, --video-description <description>', 'Video description')
.option('-t, --tags <tags>', 'Video tags', list) .option('-t, --tags <tags>', 'Video tags', list)
.option('-b, --thumbnail <thumbnailPath>', 'Thumbnail path') .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['privacy']) program['privacy'] = VideoPrivacy.PUBLIC
if (!program['commentsEnabled']) program['commentsEnabled'] = false if (!program['commentsEnabled']) program['commentsEnabled'] = false
if ( getSettings()
!program['url'] || .then(settings => {
!program['username'] || if (
!program['password'] || (!program['url'] ||
!program['videoName'] || !program['username'] ||
!program['file'] !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['url']) console.error('--url field is required.')
if (!program['password']) console.error('--password field is required.') if (!program['username']) console.error('--username field is required.')
if (!program['videoName']) console.error('--video-name field is required.') if (!program['password']) console.error('--password field is required.')
if (!program['file']) console.error('--file field is required.') if (!program['videoName']) console.error('--video-name field is required.')
process.exit(-1) if (!program['file']) console.error('--file field is required.')
} process.exit(-1)
}
if (isAbsolute(program['file']) === false) { if (
console.error('File path should be absolute.') (!program['url'] ||
process.exit(-1) !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['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 () { async function run () {
const res = await getClient(program[ 'url' ]) const res = await getClient(program[ 'url' ])

View File

@ -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
server/tools/peertube.ts Executable file
View File

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

View File

@ -1,14 +1,60 @@
# CLI tools guide # CLI tools guide
- [CLI wrapper](#cli-wrapper)
- [Remote tools](#remote-tools) - [Remote tools](#remote-tools)
- [import-videos.js](#import-videosjs) - [peertube-import-videos.js](#peertube-import-videosjs)
- [upload.js](#uploadjs) - [peertube-upload.js](#peertube-uploadjs)
- [peertube-watch.js](#peertube-watch)
- [Server tools](#server-tools) - [Server tools](#server-tools)
- [parse-log](#parse-log) - [parse-log](#parse-log)
- [create-transcoding-job.js](#create-transcoding-jobjs) - [create-transcoding-job.js](#create-transcoding-jobjs)
- [create-import-video-file-job.js](#create-import-video-file-jobjs) - [create-import-video-file-job.js](#create-import-video-file-jobjs)
- [prune-storage.js](#prune-storagejs) - [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 ## Remote Tools
You need at least 512MB RAM to run the script. You need at least 512MB RAM to run the script.
@ -40,13 +86,13 @@ $ cd ${CLONE}
$ npm run build:server $ 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. 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. Be sure you own the videos or have the author's authorization to do so.
```sh ```sh
$ node dist/server/tools/import-videos.js \ $ node dist/server/tools/peertube-import-videos.js \
-u "PEERTUBE_URL" \ -u "PEERTUBE_URL" \
-U "PEERTUBE_USER" \ -U "PEERTUBE_USER" \
--password "PEERTUBE_PASSWORD" \ --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). 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. 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} $ 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 ## Server tools

View File

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