"engines": {
"node": ">=8.x"
},
+ "bin": {
+ "peertube": "dist/server/tools/peertube.js"
+ },
"author": {
"name": "Florian Bigard",
"email": "florian.bigard@gmail.com",
"@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",
"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"
}
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) => {
})
}
-// ---------------------------------------------------------------------------
-
-export {
- downloadYoutubeDLVideo,
- getYoutubeDLInfo
-}
-
-// ---------------------------------------------------------------------------
-
async function safeGetYoutubeDL () {
let youtubeDL
return youtubeDL
}
+// ---------------------------------------------------------------------------
+
+export {
+ downloadYoutubeDLVideo,
+ getYoutubeDLInfo,
+ safeGetYoutubeDL
+}
+
+// ---------------------------------------------------------------------------
+
function normalizeObject (obj: any) {
const newObj: any = {}
// 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'
--- /dev/null
+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 ])
+ })
+})
--- /dev/null
+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
+}
+++ /dev/null
-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)
- })
+++ /dev/null
-// 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
-}
--- /dev/null
+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)
--- /dev/null
+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)
+ })
--- /dev/null
+// 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
+}
--- /dev/null
+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(',')
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+#!/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)
+ })
+++ /dev/null
-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(',')
-}
# 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.
$ 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" \
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.
```
$ 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
"sourceMap": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
+ "removeComments": true,
"outDir": "./dist",
"lib": [
"dom",