From 1d9d9cfdcf3983e3fd89026bc4b5633a8abf5752 Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Tue, 11 Sep 2018 22:55:41 +0200 Subject: [PATCH 1/2] add css custom properties for the left menu --- client/src/app/menu/menu.component.scss | 8 ++++---- client/src/sass/application.scss | 2 ++ client/src/sass/include/_variables.scss | 2 ++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss index 0f98da0e2..3e072279f 100644 --- a/client/src/app/menu/menu.component.scss +++ b/client/src/app/menu/menu.component.scss @@ -10,14 +10,14 @@ } menu { - background-color: $menu-background; + background-color: var(--menuBackgroundColor); margin: 0; padding: 0; height: 100%; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; - color: $menu-color; + color: var(--menuForegroundColor); display: flex; flex-direction: column; width: 100%; @@ -52,7 +52,7 @@ menu { .logged-in-username { font-size: 16px; font-weight: $font-semibold; - color: $menu-color; + color: var(--menuForegroundColor); cursor: pointer; @include disable-default-a-behaviour; @@ -124,7 +124,7 @@ menu { display: flex; align-items: center; padding-left: $menu-lateral-padding; - color: $menu-color; + color: var(--menuForegroundColor); cursor: pointer; height: 40px; font-size: 16px; diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss index 8d2bfb077..caf039b6d 100644 --- a/client/src/sass/application.scss +++ b/client/src/sass/application.scss @@ -26,6 +26,8 @@ body { --mainHoverColor: #{$orange-hoover-color}; --mainBackgroundColor: #{$bg-color}; --mainForegroundColor: #{$fg-color}; + --menuBackgroundColor: #{$menu-background}; + --menuForegroundColor: #{$menu-color}; --submenuColor: #{$sub-menu-color}; --inputColor: #{$input-background-color}; --inputPlaceholderColor: #{$input-placeholder-color}; diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss index 3514bdb9b..fdf33b12a 100644 --- a/client/src/sass/include/_variables.scss +++ b/client/src/sass/include/_variables.scss @@ -58,6 +58,8 @@ $variables: ( --mainHoverColor: var(--mainHoverColor), --mainBackgroundColor: var(--mainBackgroundColor), --mainForegroundColor: var(--mainForegroundColor), + --menuBackgroundColor: var(--menuBackgroundColor), + --menuForegroundColor: var(--menuForegroundColor), --submenuColor: var(--submenuColor), --inputColor: var(--inputColor), --inputPlaceholderColor: var(--inputPlaceholderColor) From 8704acf49efc770d73bf07c10468ed8c74d28a83 Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Thu, 13 Sep 2018 14:27:44 +0200 Subject: [PATCH 2/2] 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 --- package.json | 10 ++ 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/peertube-auth.ts | 140 ++++++++++++++++++ ...-token.ts => peertube-get-access-token.ts} | 5 +- ...rt-videos.ts => peertube-import-videos.ts} | 121 +++++++++------ .../tools/{upload.ts => peertube-upload.ts} | 74 ++++++--- server/tools/peertube-watch.ts | 61 ++++++++ server/tools/peertube.ts | 81 ++++++++++ support/doc/tools.md | 75 +++++++++- tsconfig.json | 1 + 13 files changed, 622 insertions(+), 84 deletions(-) create mode 100644 server/tests/cli/peertube.ts create mode 100644 server/tools/cli.ts create mode 100644 server/tools/peertube-auth.ts rename server/tools/{get-access-token.ts => peertube-get-access-token.ts} (79%) rename server/tools/{import-videos.ts => peertube-import-videos.ts} (72%) rename server/tools/{upload.ts => peertube-upload.ts} (56%) create mode 100644 server/tools/peertube-watch.ts create mode 100755 server/tools/peertube.ts diff --git a/package.json b/package.json index 5a8843b0c..cc4f6be5c 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "engines": { "node": ">=8.x" }, + "bin": { + "peertube": "dist/server/tools/peertube.js" + }, "author": { "name": "Florian Bigard", "email": "florian.bigard@gmail.com", @@ -78,6 +81,7 @@ "@types/bluebird": "3.5.21" }, "dependencies": { + "application-config": "^1.0.1", "async": "^2.0.0", "async-lock": "^1.1.2", "async-lru": "^1.1.1", @@ -86,6 +90,7 @@ "bluebird": "^3.5.0", "body-parser": "^1.12.4", "bull": "^3.4.2", + "cli-table": "^0.3.1", "bytes": "^3.0.0", "commander": "^2.13.0", "concurrently": "^4.0.1", @@ -113,6 +118,7 @@ "magnet-uri": "^5.1.4", "morgan": "^1.5.3", "multer": "^1.1.0", + "netrc-parser": "^3.1.6", "nodemailer": "^4.4.2", "parse-torrent": "^6.0.0", "password-generator": "^2.0.2", @@ -130,6 +136,7 @@ "sequelize-typescript": "0.6.6", "sharp": "^0.20.0", "srt-to-vtt": "^1.1.2", + "summon-install": "^0.4.3", "useragent": "^2.3.0", "uuid": "^3.1.0", "validator": "^10.2.0", @@ -196,5 +203,8 @@ "scripty": { "silent": true }, + "summon": { + "silent": true + }, "sasslintConfig": "client/.sass-lint.yml" } 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/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/get-access-token.ts b/server/tools/peertube-get-access-token.ts similarity index 79% rename from server/tools/get-access-token.ts rename to server/tools/peertube-get-access-token.ts index d86c84c8d..eb2571a03 100644 --- a/server/tools/get-access-token.ts +++ b/server/tools/peertube-get-access-token.ts @@ -19,7 +19,10 @@ if ( !program['username'] || !program['password'] ) { - throw new Error('All arguments are required.') + if (!program['url']) console.error('--url field is required.') + if (!program['username']) console.error('--username field is required.') + if (!program['password']) console.error('--password field is required.') + process.exit(-1) } getClient(program.url) diff --git a/server/tools/import-videos.ts b/server/tools/peertube-import-videos.ts similarity index 72% rename from server/tools/import-videos.ts rename to server/tools/peertube-import-videos.ts index 3ff194c83..13090a028 100644 --- a/server/tools/import-videos.ts +++ b/server/tools/peertube-import-videos.ts @@ -3,7 +3,6 @@ require('tls').DEFAULT_ECDH_CURVE = 'auto' import * as program from 'commander' import { join } from 'path' -import * as youtubeDL from 'youtube-dl' import { VideoPrivacy } from '../../shared/models/videos' import { doRequestAndSaveToFile } from '../helpers/requests' import { CONSTRAINTS_FIELDS } from '../initializers' @@ -11,31 +10,8 @@ import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo 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)) +import { safeGetYoutubeDL } from '../helpers/youtube-dl' +import { getSettings, netrc } from './cli' let accessToken: string let client: { id: string, secret: string } @@ -45,6 +21,61 @@ const processOptions = { 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() @@ -65,20 +96,22 @@ async function promptPassword () { }) } -async function run () { +async function run (user, url: string) { if (!user.password) { user.password = await promptPassword() } - const res = await getClient(program['url']) + const res = await getClient(url) client = { id: res.body.client_id, secret: res.body.client_secret } - const res2 = await login(program['url'], client, user) + const res2 = await login(url, client, user) accessToken = res2.body.access_token + const youtubeDL = await safeGetYoutubeDL() + const options = [ '-j', '--flat-playlist', '--playlist-reverse' ] youtubeDL.getInfo(program['targetUrl'], options, processOptions, async (err, info) => { if (err) { @@ -97,7 +130,7 @@ async function run () { console.log('Will download and upload %d videos.\n', infoArray.length) for (const info of infoArray) { - await processVideo(info, program['language']) + await processVideo(info, program['language'], processOptions.cwd, url, user) } // https://www.youtube.com/watch?v=2Upx39TBc1s @@ -106,14 +139,14 @@ async function run () { }) } -function processVideo (info: any, languageCode: string) { +function processVideo (info: any, languageCode: string, cwd: string, url: string, user) { return new Promise(async res => { if (program['verbose']) console.log('Fetching object.', info) const videoInfo = await fetchObject(info) if (program['verbose']) console.log('Fetched object.', videoInfo) - const result = await searchVideoWithSort(program['url'], videoInfo.title, '-match') + const result = await searchVideoWithSort(url, videoInfo.title, '-match') console.log('############################################################\n') @@ -122,12 +155,13 @@ function processVideo (info: any, languageCode: string) { return res() } - const path = join(__dirname, new Date().getTime() + '.mp4') + const path = join(cwd, new Date().getTime() + '.mp4') console.log('Downloading video "%s"...', videoInfo.title) const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ] try { + const youtubeDL = await safeGetYoutubeDL() youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => { if (err) { console.error(err) @@ -135,7 +169,7 @@ function processVideo (info: any, languageCode: string) { } console.log(output.join('\n')) - await uploadVideoOnPeerTube(normalizeObject(videoInfo), path, languageCode) + await uploadVideoOnPeerTube(normalizeObject(videoInfo), path, cwd, url, user, languageCode) return res() }) } catch (err) { @@ -145,8 +179,8 @@ function processVideo (info: any, languageCode: string) { }) } -async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, language?: string) { - const category = await getCategory(videoInfo.categories) +async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, cwd: string, url: string, user, language?: string) { + const category = await getCategory(videoInfo.categories, url) const licence = getLicence(videoInfo.license) let tags = [] if (Array.isArray(videoInfo.tags)) { @@ -158,7 +192,7 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, languag let thumbnailfile if (videoInfo.thumbnail) { - thumbnailfile = join(__dirname, 'thumbnail.jpg') + thumbnailfile = join(cwd, 'thumbnail.jpg') await doRequestAndSaveToFile({ method: 'GET', @@ -189,15 +223,15 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, languag console.log('\nUploading on PeerTube video "%s".', videoAttributes.name) try { - await uploadVideo(program['url'], accessToken, videoAttributes) + await uploadVideo(url, accessToken, videoAttributes) } catch (err) { if (err.message.indexOf('401') !== -1) { console.log('Got 401 Unauthorized, token may have expired, renewing token and retry.') - const res = await login(program['url'], client, user) + const res = await login(url, client, user) accessToken = res.body.access_token - await uploadVideo(program['url'], accessToken, videoAttributes) + await uploadVideo(url, accessToken, videoAttributes) } else { console.log(err.message) process.exit(1) @@ -210,14 +244,14 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, languag console.log('Uploaded video "%s"!\n', videoAttributes.name) } -async function getCategory (categories: string[]) { +async function getCategory (categories: string[], url: string) { if (!categories) return undefined const categoryString = categories[0] if (categoryString === 'News & Politics') return 11 - const res = await getVideoCategories(program['url']) + const res = await getVideoCategories(url) const categoriesServer = res.body for (const key of Object.keys(categoriesServer)) { @@ -228,6 +262,8 @@ async function getCategory (categories: string[]) { return undefined } +/* ---------------------------------------------------------- */ + function getLicence (licence: string) { if (!licence) return undefined @@ -259,6 +295,7 @@ function fetchObject (info: any) { const url = buildUrl(info) return new Promise(async (res, rej) => { + const youtubeDL = await safeGetYoutubeDL() youtubeDL.getInfo(url, undefined, processOptions, async (err, videoInfo) => { if (err) return rej(err) diff --git a/server/tools/upload.ts b/server/tools/peertube-upload.ts similarity index 56% rename from server/tools/upload.ts rename to server/tools/peertube-upload.ts index 9b104d308..1f871e660 100644 --- a/server/tools/upload.ts +++ b/server/tools/peertube-upload.ts @@ -4,18 +4,20 @@ import { isAbsolute } from 'path' import { getClient, login } from '../tests/utils' import { uploadVideo } from '../tests/utils/index' import { VideoPrivacy } from '../../shared/models/videos' +import { netrc, getSettings } from './cli' program + .name('upload') .option('-u, --url ', 'Server url') .option('-U, --username ', 'Username') .option('-p, --password ', 'Password') .option('-n, --video-name ', 'Video name') - .option('-P, --privacy ', 'Privacy') + .option('-P, --privacy ', 'Privacy') .option('-N, --nsfw', 'Video is Not Safe For Work') - .option('-c, --category ', 'Category number') + .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('-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') @@ -28,27 +30,53 @@ if (!program['nsfw']) program['nsfw'] = false if (!program['privacy']) program['privacy'] = VideoPrivacy.PUBLIC if (!program['commentsEnabled']) program['commentsEnabled'] = false -if ( - !program['url'] || - !program['username'] || - !program['password'] || - !program['videoName'] || - !program['file'] -) { - if (!program['url']) console.error('--url field is required.') - if (!program['username']) console.error('--username field is required.') - if (!program['password']) console.error('--password field is required.') - if (!program['videoName']) console.error('--video-name field is required.') - if (!program['file']) console.error('--file field is required.') - process.exit(-1) -} +getSettings() + .then(settings => { + if ( + (!program['url'] || + !program['username'] || + !program['password']) && + (settings.remotes.length === 0) + ) { + if (!program['url']) console.error('--url field is required.') + if (!program['username']) console.error('--username field is required.') + if (!program['password']) console.error('--password field is required.') + if (!program['videoName']) console.error('--video-name field is required.') + if (!program['file']) console.error('--file field is required.') + process.exit(-1) + } -if (isAbsolute(program['file']) === false) { - console.error('File path should be absolute.') - process.exit(-1) -} + if ( + (!program['url'] || + !program['username'] || + !program['password']) && + (settings.remotes.length > 0) + ) { + if (!program['url']) { + program['url'] = (settings.default !== -1) ? + settings.remotes[settings.default] : + settings.remotes[0] + } + if (!program['username']) program['username'] = netrc.machines[program['url']].login + if (!program['password']) program['password'] = netrc.machines[program['url']].password + } -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 () { const res = await getClient(program[ 'url' ]) 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/support/doc/tools.md b/support/doc/tools.md index 0a2f1f11b..1db29edc0 100644 --- a/support/doc/tools.md +++ b/support/doc/tools.md @@ -1,14 +1,60 @@ # CLI tools guide - + - [CLI wrapper](#cli-wrapper) - [Remote tools](#remote-tools) - - [import-videos.js](#import-videosjs) - - [upload.js](#uploadjs) + - [peertube-import-videos.js](#peertube-import-videosjs) + - [peertube-upload.js](#peertube-uploadjs) + - [peertube-watch.js](#peertube-watch) - [Server tools](#server-tools) - [parse-log](#parse-log) - [create-transcoding-job.js](#create-transcoding-jobjs) - [create-import-video-file-job.js](#create-import-video-file-jobjs) - [prune-storage.js](#prune-storagejs) +## CLI wrapper + +The wrapper provides a convenient interface to most scripts, and requires the [same dependencies](#dependencies). You can access it as `peertube` via an alias in your `.bashrc` like `alias peertube="node ${PEERTUBE_PATH}/dist/server/tools/peertube.js"`: + +``` + Usage: peertube [command] [options] + + Options: + + -v, --version output the version number + -h, --help output usage information + + Commands: + + auth [action] register your accounts on remote instances to use them with other commands + upload|up upload a video + import-videos|import import a video from a streaming platform + watch|w watch a video in the terminal ✩°。⋆ + help [cmd] display help for [cmd] +``` + +The wrapper can keep track of instances you have an account on. We limit to one account per instance for now. + +```bash +$ peertube auth add -u "PEERTUBE_URL" -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD" +$ peertube auth list +┌──────────────────────────────┬──────────────────────────────┐ +│ instance │ login │ +├──────────────────────────────┼──────────────────────────────┤ +│ "PEERTUBE_URL" │ "PEERTUBE_USER" │ +└──────────────────────────────┴──────────────────────────────┘ +``` + +You can now use that account to upload videos without feeding the same parameters again. + +```bash +$ peertube up +``` + +And now that your video is online, you can watch it from the confort of your terminal (use `peertube watch --help` to see the supported players): + +```bash +$ peertube watch https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10 +``` + ## Remote Tools You need at least 512MB RAM to run the script. @@ -40,13 +86,13 @@ $ cd ${CLONE} $ npm run build:server ``` -### import-videos.js +### peertube-import-videos.js You can use this script to import videos from all [supported sites of youtube-dl](https://rg3.github.io/youtube-dl/supportedsites.html) into PeerTube. Be sure you own the videos or have the author's authorization to do so. ```sh -$ node dist/server/tools/import-videos.js \ +$ node dist/server/tools/peertube-import-videos.js \ -u "PEERTUBE_URL" \ -U "PEERTUBE_USER" \ --password "PEERTUBE_PASSWORD" \ @@ -70,7 +116,7 @@ Already downloaded videos will not be uploaded twice, so you can run and re-run Videos will be publicly available after transcoding (you can see them before that in your account on the web interface). -### upload.js +### peertube-upload.js You can use this script to import videos directly from the CLI. @@ -78,9 +124,24 @@ Videos will be publicly available after transcoding (you can see them before tha ``` $ cd ${CLONE} -$ node dist/server/tools/upload.js --help +$ node dist/server/tools/peertube-upload.js --help ``` +### peertube-watch.js + +You can use this script to play videos directly from the CLI. + +It provides support for different players: + +- ascii (default ; plays in ascii art in your terminal!) +- mpv +- mplayer +- vlc +- stdout +- xbmc +- airplay +- chromecast + ## Server tools diff --git a/tsconfig.json b/tsconfig.json index 7633465b2..c84b179cf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "sourceMap": false, "experimentalDecorators": true, "emitDecoratorMetadata": true, + "removeComments": true, "outDir": "./dist", "lib": [ "dom",