From 8704acf49efc770d73bf07c10468ed8c74d28a83 Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Thu, 13 Sep 2018 14:27:44 +0200 Subject: one cli to unite them all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ash nazg thrakatulûk agh burzum-ishi krimpatul - refactor import-videos to use the youtubeDL helper - add very basic tests for the cli --- server/helpers/youtube-dl.ts | 23 ++- server/tests/cli/index.ts | 1 + server/tests/cli/peertube.ts | 51 +++++ server/tools/cli.ts | 63 ++++++ server/tools/get-access-token.ts | 48 ----- server/tools/import-videos.ts | 286 -------------------------- server/tools/peertube-auth.ts | 140 +++++++++++++ server/tools/peertube-get-access-token.ts | 51 +++++ server/tools/peertube-import-videos.ts | 323 ++++++++++++++++++++++++++++++ server/tools/peertube-upload.ts | 127 ++++++++++++ server/tools/peertube-watch.ts | 61 ++++++ server/tools/peertube.ts | 81 ++++++++ server/tools/upload.ts | 99 --------- 13 files changed, 910 insertions(+), 444 deletions(-) create mode 100644 server/tests/cli/peertube.ts create mode 100644 server/tools/cli.ts delete mode 100644 server/tools/get-access-token.ts delete mode 100644 server/tools/import-videos.ts create mode 100644 server/tools/peertube-auth.ts create mode 100644 server/tools/peertube-get-access-token.ts create mode 100644 server/tools/peertube-import-videos.ts create mode 100644 server/tools/peertube-upload.ts create mode 100644 server/tools/peertube-watch.ts create mode 100755 server/tools/peertube.ts delete mode 100644 server/tools/upload.ts (limited to 'server') diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts index 6738090f3..8b2bc1782 100644 --- a/server/helpers/youtube-dl.ts +++ b/server/helpers/youtube-dl.ts @@ -14,9 +14,9 @@ export type YoutubeDLInfo = { thumbnailUrl?: string } -function getYoutubeDLInfo (url: string): Promise { +function getYoutubeDLInfo (url: string, opts?: string[]): Promise { return new Promise(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 = {} diff --git a/server/tests/cli/index.ts b/server/tests/cli/index.ts index f99eafe03..33e33a070 100644 --- a/server/tests/cli/index.ts +++ b/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' diff --git a/server/tests/cli/peertube.ts b/server/tests/cli/peertube.ts new file mode 100644 index 000000000..548fd1257 --- /dev/null +++ b/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 ]) + }) +}) diff --git a/server/tools/cli.ts b/server/tools/cli.ts new file mode 100644 index 000000000..9a170d4da --- /dev/null +++ b/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((res, rej) => { + let settings = { + remotes: [], + default: 0 + } as Settings + config.read((err, data) => { + if (err) { + return rej(err) + } + return res(data || settings) + }) + }) +} + +async function writeSettings (settings) { + return new Promise((res, rej) => { + config.write(settings, function (err) { + if (err) { + return rej(err) + } + return res() + }) + }) +} + +netrc.loadSync() + +// --------------------------------------------------------------------------- + +export { + version, + config, + settings, + getSettings, + writeSettings, + netrc +} diff --git a/server/tools/get-access-token.ts b/server/tools/get-access-token.ts deleted file mode 100644 index d86c84c8d..000000000 --- a/server/tools/get-access-token.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as program from 'commander' - -import { - getClient, - serverLogin, - Server, - Client, - User -} from '../tests/utils/index' - -program - .option('-u, --url ', 'Server url') - .option('-n, --username ', 'Username') - .option('-p, --password ', 'Password') - .parse(process.argv) - -if ( - !program['url'] || - !program['username'] || - !program['password'] -) { - throw new Error('All arguments are required.') -} - -getClient(program.url) - .then(res => { - const server = { - url: program['url'], - user: { - username: program['username'], - password: program['password'] - } as User, - client: { - id: res.body.client_id as string, - secret: res.body.client_secret as string - } as Client - } as Server - - return serverLogin(server) - }) - .then(accessToken => { - console.log(accessToken) - process.exit(0) - }) - .catch(err => { - console.error(err) - process.exit(-1) - }) diff --git a/server/tools/import-videos.ts b/server/tools/import-videos.ts deleted file mode 100644 index 3ff194c83..000000000 --- a/server/tools/import-videos.ts +++ /dev/null @@ -1,286 +0,0 @@ -// FIXME: https://github.com/nodejs/node/pull/16853 -require('tls').DEFAULT_ECDH_CURVE = 'auto' - -import * as program from 'commander' -import { join } from 'path' -import * as youtubeDL from 'youtube-dl' -import { VideoPrivacy } from '../../shared/models/videos' -import { doRequestAndSaveToFile } from '../helpers/requests' -import { CONSTRAINTS_FIELDS } from '../initializers' -import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../tests/utils' -import { truncate } from 'lodash' -import * as prompt from 'prompt' -import { remove } from 'fs-extra' - -program - .option('-u, --url ', 'Server url') - .option('-U, --username ', 'Username') - .option('-p, --password ', 'Password') - .option('-t, --target-url ', 'Video target URL') - .option('-l, --language ', '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(async (res, rej) => { - youtubeDL.getInfo(url, undefined, processOptions, async (err, videoInfo) => { - if (err) return rej(err) - - const videoInfoWithUrl = Object.assign(videoInfo, { url }) - return res(normalizeObject(videoInfoWithUrl)) - }) - }) -} - -function buildUrl (info: any) { - const webpageUrl = info.webpage_url as string - if (webpageUrl && webpageUrl.match(/^https?:\/\//)) return webpageUrl - - const url = info.url as string - if (url && url.match(/^https?:\/\//)) return url - - // It seems youtube-dl does not return the video url - return 'https://www.youtube.com/watch?v=' + info.id -} - -function isNSFW (info: any) { - if (info.age_limit && info.age_limit >= 16) return true - - return false -} diff --git a/server/tools/peertube-auth.ts b/server/tools/peertube-auth.ts new file mode 100644 index 000000000..33438811e --- /dev/null +++ b/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 ', 'Server url') + .option('-U, --username ', 'Username') + .option('-p, --password ', '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 ') + .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 ') + .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(' is not a registered instance.') + process.exit(-1) + } + }) + }) + +program.on('--help', function () { + console.log(' Examples:') + console.log() + console.log(' $ peertube add -u peertube.cpy.re -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"') + console.log(' $ peertube add -u peertube.cpy.re -U root') + console.log(' $ peertube list') + console.log(' $ peertube del peertube.cpy.re') + console.log() +}) + +if (!process.argv.slice(2).length) { + program.outputHelp() +} + +program.parse(process.argv) diff --git a/server/tools/peertube-get-access-token.ts b/server/tools/peertube-get-access-token.ts new file mode 100644 index 000000000..eb2571a03 --- /dev/null +++ b/server/tools/peertube-get-access-token.ts @@ -0,0 +1,51 @@ +import * as program from 'commander' + +import { + getClient, + serverLogin, + Server, + Client, + User +} from '../tests/utils/index' + +program + .option('-u, --url ', 'Server url') + .option('-n, --username ', 'Username') + .option('-p, --password ', 'Password') + .parse(process.argv) + +if ( + !program['url'] || + !program['username'] || + !program['password'] +) { + if (!program['url']) console.error('--url field is required.') + if (!program['username']) console.error('--username field is required.') + if (!program['password']) console.error('--password field is required.') + process.exit(-1) +} + +getClient(program.url) + .then(res => { + const server = { + url: program['url'], + user: { + username: program['username'], + password: program['password'] + } as User, + client: { + id: res.body.client_id as string, + secret: res.body.client_secret as string + } as Client + } as Server + + return serverLogin(server) + }) + .then(accessToken => { + console.log(accessToken) + process.exit(0) + }) + .catch(err => { + console.error(err) + process.exit(-1) + }) diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts new file mode 100644 index 000000000..13090a028 --- /dev/null +++ b/server/tools/peertube-import-videos.ts @@ -0,0 +1,323 @@ +// FIXME: https://github.com/nodejs/node/pull/16853 +require('tls').DEFAULT_ECDH_CURVE = 'auto' + +import * as program from 'commander' +import { join } from 'path' +import { VideoPrivacy } from '../../shared/models/videos' +import { doRequestAndSaveToFile } from '../helpers/requests' +import { CONSTRAINTS_FIELDS } from '../initializers' +import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../tests/utils' +import { truncate } from 'lodash' +import * as prompt from 'prompt' +import { remove } from 'fs-extra' +import { safeGetYoutubeDL } from '../helpers/youtube-dl' +import { getSettings, netrc } from './cli' + +let accessToken: string +let client: { id: string, secret: string } + +const processOptions = { + cwd: __dirname, + maxBuffer: Infinity +} + +program + .name('import-videos') + .option('-u, --url ', 'Server url') + .option('-U, --username ', 'Username') + .option('-p, --password ', 'Password') + .option('-t, --target-url ', 'Video target URL') + .option('-l, --language ', '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(async (res, rej) => { + const youtubeDL = await safeGetYoutubeDL() + youtubeDL.getInfo(url, undefined, processOptions, async (err, videoInfo) => { + if (err) return rej(err) + + const videoInfoWithUrl = Object.assign(videoInfo, { url }) + return res(normalizeObject(videoInfoWithUrl)) + }) + }) +} + +function buildUrl (info: any) { + const webpageUrl = info.webpage_url as string + if (webpageUrl && webpageUrl.match(/^https?:\/\//)) return webpageUrl + + const url = info.url as string + if (url && url.match(/^https?:\/\//)) return url + + // It seems youtube-dl does not return the video url + return 'https://www.youtube.com/watch?v=' + info.id +} + +function isNSFW (info: any) { + if (info.age_limit && info.age_limit >= 16) return true + + return false +} diff --git a/server/tools/peertube-upload.ts b/server/tools/peertube-upload.ts new file mode 100644 index 000000000..1f871e660 --- /dev/null +++ b/server/tools/peertube-upload.ts @@ -0,0 +1,127 @@ +import * as program from 'commander' +import { access, constants } from 'fs-extra' +import { isAbsolute } from 'path' +import { getClient, login } from '../tests/utils' +import { uploadVideo } from '../tests/utils/index' +import { VideoPrivacy } from '../../shared/models/videos' +import { netrc, getSettings } from './cli' + +program + .name('upload') + .option('-u, --url ', 'Server url') + .option('-U, --username ', 'Username') + .option('-p, --password ', 'Password') + .option('-n, --video-name ', 'Video name') + .option('-P, --privacy ', 'Privacy') + .option('-N, --nsfw', 'Video is Not Safe For Work') + .option('-c, --category ', 'Category number') + .option('-m, --comments-enabled', 'Enable comments') + .option('-l, --licence ', 'Licence number') + .option('-L, --language ', 'Language ISO 639 code (fr or en...)') + .option('-d, --video-description ', 'Video description') + .option('-t, --tags ', 'Video tags', list) + .option('-b, --thumbnail ', 'Thumbnail path') + .option('-v, --preview ', 'Preview path') + .option('-f, --file ', 'Video absolute file path') + .parse(process.argv) + +if (!program['tags']) program['tags'] = [] +if (!program['nsfw']) program['nsfw'] = false +if (!program['privacy']) program['privacy'] = VideoPrivacy.PUBLIC +if (!program['commentsEnabled']) program['commentsEnabled'] = false + +getSettings() + .then(settings => { + if ( + (!program['url'] || + !program['username'] || + !program['password']) && + (settings.remotes.length === 0) + ) { + if (!program['url']) console.error('--url field is required.') + if (!program['username']) console.error('--username field is required.') + if (!program['password']) console.error('--password field is required.') + if (!program['videoName']) console.error('--video-name field is required.') + if (!program['file']) console.error('--file field is required.') + process.exit(-1) + } + + if ( + (!program['url'] || + !program['username'] || + !program['password']) && + (settings.remotes.length > 0) + ) { + if (!program['url']) { + program['url'] = (settings.default !== -1) ? + settings.remotes[settings.default] : + settings.remotes[0] + } + if (!program['username']) program['username'] = netrc.machines[program['url']].login + if (!program['password']) program['password'] = netrc.machines[program['url']].password + } + + if ( + !program['videoName'] || + !program['file'] + ) { + if (!program['videoName']) console.error('--video-name field is required.') + if (!program['file']) console.error('--file field is required.') + process.exit(-1) + } + + if (isAbsolute(program['file']) === false) { + console.error('File path should be absolute.') + process.exit(-1) + } + + run().catch(err => console.error(err)) + }) + +async function run () { + const res = await getClient(program[ 'url' ]) + const client = { + id: res.body.client_id, + secret: res.body.client_secret + } + + const user = { + username: program[ 'username' ], + password: program[ 'password' ] + } + + const res2 = await login(program[ 'url' ], client, user) + const accessToken = res2.body.access_token + + await access(program[ 'file' ], constants.F_OK) + + console.log('Uploading %s video...', program[ 'videoName' ]) + + const videoAttributes = { + name: program['videoName'], + category: program['category'], + licence: program['licence'], + language: program['language'], + nsfw: program['nsfw'], + description: program['videoDescription'], + tags: program['tags'], + commentsEnabled: program['commentsEnabled'], + fixture: program['file'], + thumbnailfile: program['thumbnail'], + previewfile: program['preview'], + waitTranscoding: true, + privacy: program['privacy'], + support: undefined + } + + await uploadVideo(program['url'], accessToken, videoAttributes) + + console.log(`Video ${program['videoName']} uploaded.`) + process.exit(0) +} + +// ---------------------------------------------------------------------------- + +function list (val) { + return val.split(',') +} diff --git a/server/tools/peertube-watch.ts b/server/tools/peertube-watch.ts new file mode 100644 index 000000000..bf7274aab --- /dev/null +++ b/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('') + .option('-g, --gui ', 'player type', /^(airplay|stdout|chromecast|mpv|vlc|mplayer|ascii|xbmc)$/i, 'ascii') + .option('-i, --invert', 'invert colors (ascii player only)', true) + .option('-r, --resolution ', '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(' positional argument is required.') + process.exit(-1) +} else { program['url'] = videoURL } + +handler(program) + +function handler (argv) { + if (argv['gui'] === 'ascii') { + summon('peerterminal') + const peerterminal = summon('peerterminal') + peerterminal([ '--link', videoURL, '--invert', argv['invert'] ]) + } else { + summon('webtorrent-hybrid') + const CMD = 'node ' + join(root(), 'node_modules', 'webtorrent-hybrid', 'bin', 'cmd.js') + const CMDargs = ` --${argv.gui} ` + + argv['url'].replace('videos/watch', 'download/torrents') + + `-${argv.resolution}.torrent` + execSync(CMD + CMDargs) + } +} diff --git a/server/tools/peertube.ts b/server/tools/peertube.ts new file mode 100755 index 000000000..7441161b1 --- /dev/null +++ b/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 ') + console.log(' $ peertube watch https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10') + console.log() + }) + .parse(process.argv) + }) diff --git a/server/tools/upload.ts b/server/tools/upload.ts deleted file mode 100644 index 9b104d308..000000000 --- a/server/tools/upload.ts +++ /dev/null @@ -1,99 +0,0 @@ -import * as program from 'commander' -import { access, constants } from 'fs-extra' -import { isAbsolute } from 'path' -import { getClient, login } from '../tests/utils' -import { uploadVideo } from '../tests/utils/index' -import { VideoPrivacy } from '../../shared/models/videos' - -program - .option('-u, --url ', 'Server url') - .option('-U, --username ', 'Username') - .option('-p, --password ', 'Password') - .option('-n, --video-name ', 'Video name') - .option('-P, --privacy ', 'Privacy') - .option('-N, --nsfw', 'Video is Not Safe For Work') - .option('-c, --category ', 'Category number') - .option('-m, --comments-enabled', 'Enable comments') - .option('-l, --licence ', 'Licence number') - .option('-L, --language ', 'Language ISO 639 code (fr or en...)') - .option('-d, --video-description ', 'Video description') - .option('-t, --tags ', 'Video tags', list) - .option('-b, --thumbnail ', 'Thumbnail path') - .option('-v, --preview ', 'Preview path') - .option('-f, --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(',') -} -- cgit v1.2.3