aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/tools
diff options
context:
space:
mode:
Diffstat (limited to 'server/tools')
-rw-r--r--server/tools/cli.ts62
-rw-r--r--server/tools/package.json5
-rw-r--r--server/tools/peertube-auth.ts15
-rw-r--r--server/tools/peertube-import-videos.ts95
-rw-r--r--server/tools/peertube-plugins.ts34
-rw-r--r--server/tools/peertube-redundancy.ts197
-rw-r--r--server/tools/peertube-repl.ts41
-rw-r--r--server/tools/peertube-upload.ts21
-rw-r--r--server/tools/peertube-watch.ts10
-rw-r--r--server/tools/peertube.ts24
-rw-r--r--server/tools/yarn.lock23
11 files changed, 370 insertions, 157 deletions
diff --git a/server/tools/cli.ts b/server/tools/cli.ts
index 58e2445ac..d1a631b69 100644
--- a/server/tools/cli.ts
+++ b/server/tools/cli.ts
@@ -6,6 +6,9 @@ import { getVideoChannel } from '../../shared/extra-utils/videos/video-channels'
6import { Command } from 'commander' 6import { Command } from 'commander'
7import { VideoChannel, VideoPrivacy } from '../../shared/models/videos' 7import { VideoChannel, VideoPrivacy } from '../../shared/models/videos'
8import { createLogger, format, transports } from 'winston' 8import { createLogger, format, transports } from 'winston'
9import { getMyUserInformation } from '@shared/extra-utils/users/users'
10import { User, UserRole } from '@shared/models'
11import { getAccessToken } from '@shared/extra-utils/users/login'
9 12
10let configName = 'PeerTube/CLI' 13let configName = 'PeerTube/CLI'
11if (isTestInstance()) configName += `-${getAppNumber()}` 14if (isTestInstance()) configName += `-${getAppNumber()}`
@@ -14,8 +17,21 @@ const config = require('application-config')(configName)
14 17
15const version = require('../../../package.json').version 18const version = require('../../../package.json').version
16 19
20async function getAdminTokenOrDie (url: string, username: string, password: string) {
21 const accessToken = await getAccessToken(url, username, password)
22 const resMe = await getMyUserInformation(url, accessToken)
23 const me: User = resMe.body
24
25 if (me.role !== UserRole.ADMINISTRATOR) {
26 console.error('You must be an administrator.')
27 process.exit(-1)
28 }
29
30 return accessToken
31}
32
17interface Settings { 33interface Settings {
18 remotes: any[], 34 remotes: any[]
19 default: number 35 default: number
20} 36}
21 37
@@ -74,9 +90,9 @@ function getRemoteObjectOrDie (
74 if (!program['url'] || !program['username'] || !program['password']) { 90 if (!program['url'] || !program['username'] || !program['password']) {
75 // No remote and we don't have program parameters: quit 91 // No remote and we don't have program parameters: quit
76 if (settings.remotes.length === 0 || Object.keys(netrc.machines).length === 0) { 92 if (settings.remotes.length === 0 || Object.keys(netrc.machines).length === 0) {
77 if (!program[ 'url' ]) console.error('--url field is required.') 93 if (!program['url']) console.error('--url field is required.')
78 if (!program[ 'username' ]) console.error('--username field is required.') 94 if (!program['username']) console.error('--username field is required.')
79 if (!program[ 'password' ]) console.error('--password field is required.') 95 if (!program['password']) console.error('--password field is required.')
80 96
81 return process.exit(-1) 97 return process.exit(-1)
82 } 98 }
@@ -96,9 +112,9 @@ function getRemoteObjectOrDie (
96 } 112 }
97 113
98 return { 114 return {
99 url: program[ 'url' ], 115 url: program['url'],
100 username: program[ 'username' ], 116 username: program['username'],
101 password: program[ 'password' ] 117 password: program['password']
102 } 118 }
103} 119}
104 120
@@ -134,8 +150,8 @@ async function buildVideoAttributesFromCommander (url: string, command: Command,
134 const booleanAttributes: { [id in keyof typeof defaultBooleanAttributes]: boolean } | {} = {} 150 const booleanAttributes: { [id in keyof typeof defaultBooleanAttributes]: boolean } | {} = {}
135 151
136 for (const key of Object.keys(defaultBooleanAttributes)) { 152 for (const key of Object.keys(defaultBooleanAttributes)) {
137 if (command[ key ] !== undefined) { 153 if (command[key] !== undefined) {
138 booleanAttributes[key] = command[ key ] 154 booleanAttributes[key] = command[key]
139 } else if (defaultAttributes[key] !== undefined) { 155 } else if (defaultAttributes[key] !== undefined) {
140 booleanAttributes[key] = defaultAttributes[key] 156 booleanAttributes[key] = defaultAttributes[key]
141 } else { 157 } else {
@@ -144,19 +160,19 @@ async function buildVideoAttributesFromCommander (url: string, command: Command,
144 } 160 }
145 161
146 const videoAttributes = { 162 const videoAttributes = {
147 name: command[ 'videoName' ] || defaultAttributes.name, 163 name: command['videoName'] || defaultAttributes.name,
148 category: command[ 'category' ] || defaultAttributes.category || undefined, 164 category: command['category'] || defaultAttributes.category || undefined,
149 licence: command[ 'licence' ] || defaultAttributes.licence || undefined, 165 licence: command['licence'] || defaultAttributes.licence || undefined,
150 language: command[ 'language' ] || defaultAttributes.language || undefined, 166 language: command['language'] || defaultAttributes.language || undefined,
151 privacy: command[ 'privacy' ] || defaultAttributes.privacy || VideoPrivacy.PUBLIC, 167 privacy: command['privacy'] || defaultAttributes.privacy || VideoPrivacy.PUBLIC,
152 support: command[ 'support' ] || defaultAttributes.support || undefined, 168 support: command['support'] || defaultAttributes.support || undefined,
153 description: command[ 'videoDescription' ] || defaultAttributes.description || undefined, 169 description: command['videoDescription'] || defaultAttributes.description || undefined,
154 tags: command[ 'tags' ] || defaultAttributes.tags || undefined 170 tags: command['tags'] || defaultAttributes.tags || undefined
155 } 171 }
156 172
157 Object.assign(videoAttributes, booleanAttributes) 173 Object.assign(videoAttributes, booleanAttributes)
158 174
159 if (command[ 'channelName' ]) { 175 if (command['channelName']) {
160 const res = await getVideoChannel(url, command['channelName']) 176 const res = await getVideoChannel(url, command['channelName'])
161 const videoChannel: VideoChannel = res.body 177 const videoChannel: VideoChannel = res.body
162 178
@@ -172,9 +188,9 @@ async function buildVideoAttributesFromCommander (url: string, command: Command,
172 188
173function getServerCredentials (program: any) { 189function getServerCredentials (program: any) {
174 return Promise.all([ getSettings(), getNetrc() ]) 190 return Promise.all([ getSettings(), getNetrc() ])
175 .then(([ settings, netrc ]) => { 191 .then(([ settings, netrc ]) => {
176 return getRemoteObjectOrDie(program, settings, netrc) 192 return getRemoteObjectOrDie(program, settings, netrc)
177 }) 193 })
178} 194}
179 195
180function getLogger (logLevel = 'info') { 196function getLogger (logLevel = 'info') {
@@ -222,5 +238,7 @@ export {
222 getServerCredentials, 238 getServerCredentials,
223 239
224 buildCommonVideoOptions, 240 buildCommonVideoOptions,
225 buildVideoAttributesFromCommander 241 buildVideoAttributesFromCommander,
242
243 getAdminTokenOrDie
226} 244}
diff --git a/server/tools/package.json b/server/tools/package.json
index 40959d76e..06ad31cab 100644
--- a/server/tools/package.json
+++ b/server/tools/package.json
@@ -4,11 +4,12 @@
4 "private": true, 4 "private": true,
5 "dependencies": { 5 "dependencies": {
6 "application-config": "^1.0.1", 6 "application-config": "^1.0.1",
7 "cli-table": "^0.3.1", 7 "cli-table3": "^0.5.1",
8 "netrc-parser": "^3.1.6", 8 "netrc-parser": "^3.1.6",
9 "webtorrent-hybrid": "^4.0.1" 9 "webtorrent-hybrid": "^4.0.1"
10 }, 10 },
11 "summon": { 11 "summon": {
12 "silent": true 12 "silent": true
13 } 13 },
14 "devDependencies": {}
14} 15}
diff --git a/server/tools/peertube-auth.ts b/server/tools/peertube-auth.ts
index 6597a5c36..6b486e575 100644
--- a/server/tools/peertube-auth.ts
+++ b/server/tools/peertube-auth.ts
@@ -1,3 +1,5 @@
1// eslint-disable @typescript-eslint/no-unnecessary-type-assertion
2
1import { registerTSPaths } from '../helpers/register-ts-paths' 3import { registerTSPaths } from '../helpers/register-ts-paths'
2registerTSPaths() 4registerTSPaths()
3 5
@@ -5,9 +7,8 @@ import * as program from 'commander'
5import * as prompt from 'prompt' 7import * as prompt from 'prompt'
6import { getNetrc, getSettings, writeSettings } from './cli' 8import { getNetrc, getSettings, writeSettings } from './cli'
7import { isUserUsernameValid } from '../helpers/custom-validators/users' 9import { isUserUsernameValid } from '../helpers/custom-validators/users'
8import { getAccessToken, login } from '../../shared/extra-utils' 10import { getAccessToken } from '../../shared/extra-utils'
9 11import * as CliTable3 from 'cli-table3'
10const Table = require('cli-table')
11 12
12async function delInstance (url: string) { 13async function delInstance (url: string) {
13 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) 14 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
@@ -108,10 +109,10 @@ program
108 .action(async () => { 109 .action(async () => {
109 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) 110 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
110 111
111 const table = new Table({ 112 const table = new CliTable3({
112 head: ['instance', 'login'], 113 head: [ 'instance', 'login' ],
113 colWidths: [30, 30] 114 colWidths: [ 30, 30 ]
114 }) 115 }) as any
115 116
116 settings.remotes.forEach(element => { 117 settings.remotes.forEach(element => {
117 if (!netrc.machines[element]) return 118 if (!netrc.machines[element]) return
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts
index eaa792763..3fb9979df 100644
--- a/server/tools/peertube-import-videos.ts
+++ b/server/tools/peertube-import-videos.ts
@@ -1,10 +1,6 @@
1import { registerTSPaths } from '../helpers/register-ts-paths' 1import { registerTSPaths } from '../helpers/register-ts-paths'
2
3registerTSPaths() 2registerTSPaths()
4 3
5// FIXME: https://github.com/nodejs/node/pull/16853
6require('tls').DEFAULT_ECDH_CURVE = 'auto'
7
8import * as program from 'commander' 4import * as program from 'commander'
9import { join } from 'path' 5import { join } from 'path'
10import { doRequestAndSaveToFile } from '../helpers/requests' 6import { doRequestAndSaveToFile } from '../helpers/requests'
@@ -16,7 +12,7 @@ import { accessSync, constants } from 'fs'
16import { remove } from 'fs-extra' 12import { remove } from 'fs-extra'
17import { sha256 } from '../helpers/core-utils' 13import { sha256 } from '../helpers/core-utils'
18import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl' 14import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl'
19import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getServerCredentials, getLogger } from './cli' 15import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getLogger, getServerCredentials } from './cli'
20 16
21type UserInfo = { 17type UserInfo = {
22 username: string 18 username: string
@@ -42,32 +38,32 @@ command
42 .option('--first <first>', 'Process first n elements of returned playlist') 38 .option('--first <first>', 'Process first n elements of returned playlist')
43 .option('--last <last>', 'Process last n elements of returned playlist') 39 .option('--last <last>', 'Process last n elements of returned playlist')
44 .option('-T, --tmpdir <tmpdir>', 'Working directory', __dirname) 40 .option('-T, --tmpdir <tmpdir>', 'Working directory', __dirname)
41 .usage("[global options] [ -- youtube-dl options]")
45 .parse(process.argv) 42 .parse(process.argv)
46 43
47let log = getLogger(program[ 'verbose' ]) 44const log = getLogger(program['verbose'])
48 45
49getServerCredentials(command) 46getServerCredentials(command)
50 .then(({ url, username, password }) => { 47 .then(({ url, username, password }) => {
51 if (!program[ 'targetUrl' ]) { 48 if (!program['targetUrl']) {
52 exitError('--target-url field is required.') 49 exitError('--target-url field is required.')
53 } 50 }
54 51
55 try { 52 try {
56 accessSync(program[ 'tmpdir' ], constants.R_OK | constants.W_OK) 53 accessSync(program['tmpdir'], constants.R_OK | constants.W_OK)
57 } catch (e) { 54 } catch (e) {
58 exitError('--tmpdir %s: directory does not exist or is not accessible', program[ 'tmpdir' ]) 55 exitError('--tmpdir %s: directory does not exist or is not accessible', program['tmpdir'])
59 } 56 }
60 57
61 url = normalizeTargetUrl(url) 58 url = normalizeTargetUrl(url)
62 program[ 'targetUrl' ] = normalizeTargetUrl(program[ 'targetUrl' ]) 59 program['targetUrl'] = normalizeTargetUrl(program['targetUrl'])
63 60
64 const user = { username, password } 61 const user = { username, password }
65 62
66 run(url, user) 63 run(url, user)
67 .catch(err => { 64 .catch(err => exitError(err))
68 exitError(err)
69 })
70 }) 65 })
66 .catch(err => console.error(err))
71 67
72async function run (url: string, user: UserInfo) { 68async function run (url: string, user: UserInfo) {
73 if (!user.password) { 69 if (!user.password) {
@@ -76,20 +72,21 @@ async function run (url: string, user: UserInfo) {
76 72
77 const youtubeDL = await safeGetYoutubeDL() 73 const youtubeDL = await safeGetYoutubeDL()
78 74
79 const options = [ '-j', '--flat-playlist', '--playlist-reverse' ] 75 const options = [ '-j', '--flat-playlist', '--playlist-reverse', ...command.args ]
80 youtubeDL.getInfo(program[ 'targetUrl' ], options, processOptions, async (err, info) => { 76
77 youtubeDL.getInfo(program['targetUrl'], options, processOptions, async (err, info) => {
81 if (err) { 78 if (err) {
82 exitError(err.message) 79 exitError(err.stderr + ' ' + err.message)
83 } 80 }
84 81
85 let infoArray: any[] 82 let infoArray: any[]
86 83
87 // Normalize utf8 fields 84 // Normalize utf8 fields
88 infoArray = [].concat(info) 85 infoArray = [].concat(info)
89 if (program[ 'first' ]) { 86 if (program['first']) {
90 infoArray = infoArray.slice(0, program[ 'first' ]) 87 infoArray = infoArray.slice(0, program['first'])
91 } else if (program[ 'last' ]) { 88 } else if (program['last']) {
92 infoArray = infoArray.slice(-program[ 'last' ]) 89 infoArray = infoArray.slice(-program['last'])
93 } 90 }
94 infoArray = infoArray.map(i => normalizeObject(i)) 91 infoArray = infoArray.map(i => normalizeObject(i))
95 92
@@ -97,22 +94,22 @@ async function run (url: string, user: UserInfo) {
97 94
98 for (const info of infoArray) { 95 for (const info of infoArray) {
99 await processVideo({ 96 await processVideo({
100 cwd: program[ 'tmpdir' ], 97 cwd: program['tmpdir'],
101 url, 98 url,
102 user, 99 user,
103 youtubeInfo: info 100 youtubeInfo: info
104 }) 101 })
105 } 102 }
106 103
107 log.info('Video/s for user %s imported: %s', user.username, program[ 'targetUrl' ]) 104 log.info('Video/s for user %s imported: %s', user.username, program['targetUrl'])
108 process.exit(0) 105 process.exit(0)
109 }) 106 })
110} 107}
111 108
112function processVideo (parameters: { 109function processVideo (parameters: {
113 cwd: string, 110 cwd: string
114 url: string, 111 url: string
115 user: { username: string, password: string }, 112 user: { username: string, password: string }
116 youtubeInfo: any 113 youtubeInfo: any
117}) { 114}) {
118 const { youtubeInfo, cwd, url, user } = parameters 115 const { youtubeInfo, cwd, url, user } = parameters
@@ -123,17 +120,17 @@ function processVideo (parameters: {
123 const videoInfo = await fetchObject(youtubeInfo) 120 const videoInfo = await fetchObject(youtubeInfo)
124 log.debug('Fetched object.', videoInfo) 121 log.debug('Fetched object.', videoInfo)
125 122
126 if (program[ 'since' ]) { 123 if (program['since']) {
127 if (buildOriginallyPublishedAt(videoInfo).getTime() < program[ 'since' ].getTime()) { 124 if (buildOriginallyPublishedAt(videoInfo).getTime() < program['since'].getTime()) {
128 log.info('Video "%s" has been published before "%s", don\'t upload it.\n', 125 log.info('Video "%s" has been published before "%s", don\'t upload it.\n',
129 videoInfo.title, formatDate(program[ 'since' ])) 126 videoInfo.title, formatDate(program['since']))
130 return res() 127 return res()
131 } 128 }
132 } 129 }
133 if (program[ 'until' ]) { 130 if (program['until']) {
134 if (buildOriginallyPublishedAt(videoInfo).getTime() > program[ 'until' ].getTime()) { 131 if (buildOriginallyPublishedAt(videoInfo).getTime() > program['until'].getTime()) {
135 log.info('Video "%s" has been published after "%s", don\'t upload it.\n', 132 log.info('Video "%s" has been published after "%s", don\'t upload it.\n',
136 videoInfo.title, formatDate(program[ 'until' ])) 133 videoInfo.title, formatDate(program['until']))
137 return res() 134 return res()
138 } 135 }
139 } 136 }
@@ -151,7 +148,7 @@ function processVideo (parameters: {
151 148
152 log.info('Downloading video "%s"...', videoInfo.title) 149 log.info('Downloading video "%s"...', videoInfo.title)
153 150
154 const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ] 151 const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', ...command.args, '-o', path ]
155 try { 152 try {
156 const youtubeDL = await safeGetYoutubeDL() 153 const youtubeDL = await safeGetYoutubeDL()
157 youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => { 154 youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => {
@@ -178,11 +175,11 @@ function processVideo (parameters: {
178} 175}
179 176
180async function uploadVideoOnPeerTube (parameters: { 177async function uploadVideoOnPeerTube (parameters: {
181 videoInfo: any, 178 videoInfo: any
182 videoPath: string, 179 videoPath: string
183 cwd: string, 180 cwd: string
184 url: string, 181 url: string
185 user: { username: string; password: string } 182 user: { username: string, password: string }
186}) { 183}) {
187 const { videoInfo, videoPath, cwd, url, user } = parameters 184 const { videoInfo, videoPath, cwd, url, user } = parameters
188 185
@@ -210,9 +207,9 @@ async function uploadVideoOnPeerTube (parameters: {
210 207
211 const defaultAttributes = { 208 const defaultAttributes = {
212 name: truncate(videoInfo.title, { 209 name: truncate(videoInfo.title, {
213 'length': CONSTRAINTS_FIELDS.VIDEOS.NAME.max, 210 length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max,
214 'separator': /,? +/, 211 separator: /,? +/,
215 'omission': ' […]' 212 omission: ' […]'
216 }), 213 }),
217 category, 214 category,
218 licence, 215 licence,
@@ -259,7 +256,7 @@ async function uploadVideoOnPeerTube (parameters: {
259async function getCategory (categories: string[], url: string) { 256async function getCategory (categories: string[], url: string) {
260 if (!categories) return undefined 257 if (!categories) return undefined
261 258
262 const categoryString = categories[ 0 ] 259 const categoryString = categories[0]
263 260
264 if (categoryString === 'News & Politics') return 11 261 if (categoryString === 'News & Politics') return 11
265 262
@@ -267,7 +264,7 @@ async function getCategory (categories: string[], url: string) {
267 const categoriesServer = res.body 264 const categoriesServer = res.body
268 265
269 for (const key of Object.keys(categoriesServer)) { 266 for (const key of Object.keys(categoriesServer)) {
270 const categoryServer = categoriesServer[ key ] 267 const categoryServer = categoriesServer[key]
271 if (categoryString.toLowerCase() === categoryServer.toLowerCase()) return parseInt(key, 10) 268 if (categoryString.toLowerCase() === categoryServer.toLowerCase()) return parseInt(key, 10)
272 } 269 }
273 270
@@ -289,12 +286,12 @@ function normalizeObject (obj: any) {
289 // Deprecated key 286 // Deprecated key
290 if (key === 'resolution') continue 287 if (key === 'resolution') continue
291 288
292 const value = obj[ key ] 289 const value = obj[key]
293 290
294 if (typeof value === 'string') { 291 if (typeof value === 'string') {
295 newObj[ key ] = value.normalize() 292 newObj[key] = value.normalize()
296 } else { 293 } else {
297 newObj[ key ] = value 294 newObj[key] = value
298 } 295 }
299 } 296 }
300 297
@@ -306,7 +303,7 @@ function fetchObject (info: any) {
306 303
307 return new Promise<any>(async (res, rej) => { 304 return new Promise<any>(async (res, rej) => {
308 const youtubeDL = await safeGetYoutubeDL() 305 const youtubeDL = await safeGetYoutubeDL()
309 youtubeDL.getInfo(url, undefined, processOptions, async (err, videoInfo) => { 306 youtubeDL.getInfo(url, undefined, processOptions, (err, videoInfo) => {
310 if (err) return rej(err) 307 if (err) return rej(err)
311 308
312 const videoInfoWithUrl = Object.assign(videoInfo, { url }) 309 const videoInfoWithUrl = Object.assign(videoInfo, { url })
@@ -317,10 +314,10 @@ function fetchObject (info: any) {
317 314
318function buildUrl (info: any) { 315function buildUrl (info: any) {
319 const webpageUrl = info.webpage_url as string 316 const webpageUrl = info.webpage_url as string
320 if (webpageUrl && webpageUrl.match(/^https?:\/\//)) return webpageUrl 317 if (webpageUrl?.match(/^https?:\/\//)) return webpageUrl
321 318
322 const url = info.url as string 319 const url = info.url as string
323 if (url && url.match(/^https?:\/\//)) return url 320 if (url?.match(/^https?:\/\//)) return url
324 321
325 // It seems youtube-dl does not return the video url 322 // It seems youtube-dl does not return the video url
326 return 'https://www.youtube.com/watch?v=' + info.id 323 return 'https://www.youtube.com/watch?v=' + info.id
@@ -388,7 +385,7 @@ function parseDate (dateAsStr: string): Date {
388} 385}
389 386
390function formatDate (date: Date): string { 387function formatDate (date: Date): string {
391 return date.toISOString().split('T')[ 0 ] 388 return date.toISOString().split('T')[0]
392} 389}
393 390
394function exitError (message: string, ...meta: any[]) { 391function exitError (message: string, ...meta: any[]) {
diff --git a/server/tools/peertube-plugins.ts b/server/tools/peertube-plugins.ts
index e40606107..05b75fab2 100644
--- a/server/tools/peertube-plugins.ts
+++ b/server/tools/peertube-plugins.ts
@@ -1,17 +1,15 @@
1// eslint-disable @typescript-eslint/no-unnecessary-type-assertion
2
1import { registerTSPaths } from '../helpers/register-ts-paths' 3import { registerTSPaths } from '../helpers/register-ts-paths'
2registerTSPaths() 4registerTSPaths()
3 5
4import * as program from 'commander' 6import * as program from 'commander'
5import { PluginType } from '../../shared/models/plugins/plugin.type' 7import { PluginType } from '../../shared/models/plugins/plugin.type'
6import { getAccessToken } from '../../shared/extra-utils/users/login'
7import { getMyUserInformation } from '../../shared/extra-utils/users/users'
8import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins' 8import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins'
9import { getServerCredentials } from './cli' 9import { getAdminTokenOrDie, getServerCredentials } from './cli'
10import { User, UserRole } from '../../shared/models/users'
11import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model' 10import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model'
12import { isAbsolute } from 'path' 11import { isAbsolute } from 'path'
13 12import * as CliTable3 from 'cli-table3'
14const Table = require('cli-table')
15 13
16program 14program
17 .name('plugins') 15 .name('plugins')
@@ -82,10 +80,10 @@ async function pluginsListCLI () {
82 }) 80 })
83 const plugins: PeerTubePlugin[] = res.body.data 81 const plugins: PeerTubePlugin[] = res.body.data
84 82
85 const table = new Table({ 83 const table = new CliTable3({
86 head: ['name', 'version', 'homepage'], 84 head: [ 'name', 'version', 'homepage' ],
87 colWidths: [ 50, 10, 50 ] 85 colWidths: [ 50, 10, 50 ]
88 }) 86 }) as any
89 87
90 for (const plugin of plugins) { 88 for (const plugin of plugins) {
91 const npmName = plugin.type === PluginType.PLUGIN 89 const npmName = plugin.type === PluginType.PLUGIN
@@ -128,7 +126,6 @@ async function installPluginCLI (options: any) {
128 } catch (err) { 126 } catch (err) {
129 console.error('Cannot install plugin.', err) 127 console.error('Cannot install plugin.', err)
130 process.exit(-1) 128 process.exit(-1)
131 return
132 } 129 }
133 130
134 console.log('Plugin installed.') 131 console.log('Plugin installed.')
@@ -160,7 +157,6 @@ async function updatePluginCLI (options: any) {
160 } catch (err) { 157 } catch (err) {
161 console.error('Cannot update plugin.', err) 158 console.error('Cannot update plugin.', err)
162 process.exit(-1) 159 process.exit(-1)
163 return
164 } 160 }
165 161
166 console.log('Plugin updated.') 162 console.log('Plugin updated.')
@@ -181,27 +177,13 @@ async function uninstallPluginCLI (options: any) {
181 await uninstallPlugin({ 177 await uninstallPlugin({
182 url, 178 url,
183 accessToken, 179 accessToken,
184 npmName: options[ 'npmName' ] 180 npmName: options['npmName']
185 }) 181 })
186 } catch (err) { 182 } catch (err) {
187 console.error('Cannot uninstall plugin.', err) 183 console.error('Cannot uninstall plugin.', err)
188 process.exit(-1) 184 process.exit(-1)
189 return
190 } 185 }
191 186
192 console.log('Plugin uninstalled.') 187 console.log('Plugin uninstalled.')
193 process.exit(0) 188 process.exit(0)
194} 189}
195
196async function getAdminTokenOrDie (url: string, username: string, password: string) {
197 const accessToken = await getAccessToken(url, username, password)
198 const resMe = await getMyUserInformation(url, accessToken)
199 const me: User = resMe.body
200
201 if (me.role !== UserRole.ADMINISTRATOR) {
202 console.error('Cannot list plugins if you are not administrator.')
203 process.exit(-1)
204 }
205
206 return accessToken
207}
diff --git a/server/tools/peertube-redundancy.ts b/server/tools/peertube-redundancy.ts
new file mode 100644
index 000000000..1ab58a438
--- /dev/null
+++ b/server/tools/peertube-redundancy.ts
@@ -0,0 +1,197 @@
1// eslint-disable @typescript-eslint/no-unnecessary-type-assertion
2
3import { registerTSPaths } from '../helpers/register-ts-paths'
4registerTSPaths()
5
6import * as program from 'commander'
7import { getAdminTokenOrDie, getServerCredentials } from './cli'
8import { VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
9import { addVideoRedundancy, listVideoRedundancies, removeVideoRedundancy } from '@shared/extra-utils/server/redundancy'
10import validator from 'validator'
11import * as CliTable3 from 'cli-table3'
12import { URL } from 'url'
13import { uniq } from 'lodash'
14
15import bytes = require('bytes')
16
17program
18 .name('plugins')
19 .usage('[command] [options]')
20
21program
22 .command('list-remote-redundancies')
23 .description('List remote redundancies on your videos')
24 .option('-u, --url <url>', 'Server url')
25 .option('-U, --username <username>', 'Username')
26 .option('-p, --password <token>', 'Password')
27 .action(() => listRedundanciesCLI('my-videos'))
28
29program
30 .command('list-my-redundancies')
31 .description('List your redundancies of remote videos')
32 .option('-u, --url <url>', 'Server url')
33 .option('-U, --username <username>', 'Username')
34 .option('-p, --password <token>', 'Password')
35 .action(() => listRedundanciesCLI('remote-videos'))
36
37program
38 .command('add')
39 .description('Duplicate a video in your redundancy system')
40 .option('-u, --url <url>', 'Server url')
41 .option('-U, --username <username>', 'Username')
42 .option('-p, --password <token>', 'Password')
43 .option('-v, --video <videoId>', 'Video id to duplicate')
44 .action((options) => addRedundancyCLI(options))
45
46program
47 .command('remove')
48 .description('Remove a video from your redundancies')
49 .option('-u, --url <url>', 'Server url')
50 .option('-U, --username <username>', 'Username')
51 .option('-p, --password <token>', 'Password')
52 .option('-v, --video <videoId>', 'Video id to remove from redundancies')
53 .action((options) => removeRedundancyCLI(options))
54
55if (!process.argv.slice(2).length) {
56 program.outputHelp()
57}
58
59program.parse(process.argv)
60
61// ----------------------------------------------------------------------------
62
63async function listRedundanciesCLI (target: VideoRedundanciesTarget) {
64 const { url, username, password } = await getServerCredentials(program)
65 const accessToken = await getAdminTokenOrDie(url, username, password)
66
67 const redundancies = await listVideoRedundanciesData(url, accessToken, target)
68
69 const table = new CliTable3({
70 head: [ 'video id', 'video name', 'video url', 'files', 'playlists', 'by instances', 'total size' ]
71 }) as any
72
73 for (const redundancy of redundancies) {
74 const webtorrentFiles = redundancy.redundancies.files
75 const streamingPlaylists = redundancy.redundancies.streamingPlaylists
76
77 let totalSize = ''
78 if (target === 'remote-videos') {
79 const tmp = webtorrentFiles.concat(streamingPlaylists)
80 .reduce((a, b) => a + b.size, 0)
81
82 totalSize = bytes(tmp)
83 }
84
85 const instances = uniq(
86 webtorrentFiles.concat(streamingPlaylists)
87 .map(r => r.fileUrl)
88 .map(u => new URL(u).host)
89 )
90
91 table.push([
92 redundancy.id.toString(),
93 redundancy.name,
94 redundancy.url,
95 webtorrentFiles.length,
96 streamingPlaylists.length,
97 instances.join('\n'),
98 totalSize
99 ])
100 }
101
102 console.log(table.toString())
103 process.exit(0)
104}
105
106async function addRedundancyCLI (options: { videoId: number }) {
107 const { url, username, password } = await getServerCredentials(program)
108 const accessToken = await getAdminTokenOrDie(url, username, password)
109
110 if (!options['video'] || validator.isInt('' + options['video']) === false) {
111 console.error('You need to specify the video id to duplicate and it should be a number.\n')
112 program.outputHelp()
113 process.exit(-1)
114 }
115
116 try {
117 await addVideoRedundancy({
118 url,
119 accessToken,
120 videoId: options['video']
121 })
122
123 console.log('Video will be duplicated by your instance!')
124
125 process.exit(0)
126 } catch (err) {
127 if (err.message.includes(409)) {
128 console.error('This video is already duplicated by your instance.')
129 } else if (err.message.includes(404)) {
130 console.error('This video id does not exist.')
131 } else {
132 console.error(err)
133 }
134
135 process.exit(-1)
136 }
137}
138
139async function removeRedundancyCLI (options: { videoId: number }) {
140 const { url, username, password } = await getServerCredentials(program)
141 const accessToken = await getAdminTokenOrDie(url, username, password)
142
143 if (!options['video'] || validator.isInt('' + options['video']) === false) {
144 console.error('You need to specify the video id to remove from your redundancies.\n')
145 program.outputHelp()
146 process.exit(-1)
147 }
148
149 const videoId = parseInt(options['video'] + '', 10)
150
151 let redundancies = await listVideoRedundanciesData(url, accessToken, 'my-videos')
152 let videoRedundancy = redundancies.find(r => videoId === r.id)
153
154 if (!videoRedundancy) {
155 redundancies = await listVideoRedundanciesData(url, accessToken, 'remote-videos')
156 videoRedundancy = redundancies.find(r => videoId === r.id)
157 }
158
159 if (!videoRedundancy) {
160 console.error('Video redundancy not found.')
161 process.exit(-1)
162 }
163
164 try {
165 const ids = videoRedundancy.redundancies.files
166 .concat(videoRedundancy.redundancies.streamingPlaylists)
167 .map(r => r.id)
168
169 for (const id of ids) {
170 await removeVideoRedundancy({
171 url,
172 accessToken,
173 redundancyId: id
174 })
175 }
176
177 console.log('Video redundancy removed!')
178
179 process.exit(0)
180 } catch (err) {
181 console.error(err)
182 process.exit(-1)
183 }
184}
185
186async function listVideoRedundanciesData (url: string, accessToken: string, target: VideoRedundanciesTarget) {
187 const res = await listVideoRedundancies({
188 url,
189 accessToken,
190 start: 0,
191 count: 100,
192 sort: 'name',
193 target
194 })
195
196 return res.body.data as VideoRedundancy[]
197}
diff --git a/server/tools/peertube-repl.ts b/server/tools/peertube-repl.ts
index ab6e215d9..7c936ae0d 100644
--- a/server/tools/peertube-repl.ts
+++ b/server/tools/peertube-repl.ts
@@ -1,6 +1,4 @@
1import { registerTSPaths } from '../helpers/register-ts-paths' 1import { registerTSPaths } from '../helpers/register-ts-paths'
2registerTSPaths()
3
4import * as repl from 'repl' 2import * as repl from 'repl'
5import * as path from 'path' 3import * as path from 'path'
6import * as _ from 'lodash' 4import * as _ from 'lodash'
@@ -23,6 +21,8 @@ import * as signupUtils from '../helpers/signup'
23import * as utils from '../helpers/utils' 21import * as utils from '../helpers/utils'
24import * as YoutubeDLUtils from '../helpers/youtube-dl' 22import * as YoutubeDLUtils from '../helpers/youtube-dl'
25 23
24registerTSPaths()
25
26const start = async () => { 26const start = async () => {
27 await initDatabaseModels(true) 27 await initDatabaseModels(true)
28 28
@@ -31,22 +31,39 @@ const start = async () => {
31 const initContext = (replServer) => { 31 const initContext = (replServer) => {
32 return (context) => { 32 return (context) => {
33 const properties = { 33 const properties = {
34 context, repl: replServer, env: process.env, 34 context,
35 lodash: _, path, 35 repl: replServer,
36 uuidv1, uuidv3, uuidv4, uuidv5, 36 env: process.env,
37 cli, logger, constants, 37 lodash: _,
38 Sequelize, sequelizeTypescript, modelsUtils, 38 path,
39 models: sequelizeTypescript.models, transaction: sequelizeTypescript.transaction, 39 uuidv1,
40 query: sequelizeTypescript.query, queryInterface: sequelizeTypescript.getQueryInterface(), 40 uuidv3,
41 uuidv4,
42 uuidv5,
43 cli,
44 logger,
45 constants,
46 Sequelize,
47 sequelizeTypescript,
48 modelsUtils,
49 models: sequelizeTypescript.models,
50 transaction: sequelizeTypescript.transaction,
51 query: sequelizeTypescript.query,
52 queryInterface: sequelizeTypescript.getQueryInterface(),
41 YoutubeDL, 53 YoutubeDL,
42 coreUtils, ffmpegUtils, peertubeCryptoUtils, signupUtils, utils, YoutubeDLUtils 54 coreUtils,
55 ffmpegUtils,
56 peertubeCryptoUtils,
57 signupUtils,
58 utils,
59 YoutubeDLUtils
43 } 60 }
44 61
45 for (let prop in properties) { 62 for (const prop in properties) {
46 Object.defineProperty(context, prop, { 63 Object.defineProperty(context, prop, {
47 configurable: false, 64 configurable: false,
48 enumerable: true, 65 enumerable: true,
49 value: properties[ prop ] 66 value: properties[prop]
50 }) 67 })
51 } 68 }
52 } 69 }
diff --git a/server/tools/peertube-upload.ts b/server/tools/peertube-upload.ts
index f604c9bee..8de952e7b 100644
--- a/server/tools/peertube-upload.ts
+++ b/server/tools/peertube-upload.ts
@@ -24,14 +24,14 @@ command
24 24
25getServerCredentials(command) 25getServerCredentials(command)
26 .then(({ url, username, password }) => { 26 .then(({ url, username, password }) => {
27 if (!program[ 'videoName' ] || !program[ 'file' ]) { 27 if (!program['videoName'] || !program['file']) {
28 if (!program[ 'videoName' ]) console.error('--video-name is required.') 28 if (!program['videoName']) console.error('--video-name is required.')
29 if (!program[ 'file' ]) console.error('--file is required.') 29 if (!program['file']) console.error('--file is required.')
30 30
31 process.exit(-1) 31 process.exit(-1)
32 } 32 }
33 33
34 if (isAbsolute(program[ 'file' ]) === false) { 34 if (isAbsolute(program['file']) === false) {
35 console.error('File path should be absolute.') 35 console.error('File path should be absolute.')
36 process.exit(-1) 36 process.exit(-1)
37 } 37 }
@@ -41,25 +41,26 @@ getServerCredentials(command)
41 process.exit(-1) 41 process.exit(-1)
42 }) 42 })
43 }) 43 })
44 .catch(err => console.error(err))
44 45
45async function run (url: string, username: string, password: string) { 46async function run (url: string, username: string, password: string) {
46 const accessToken = await getAccessToken(url, username, password) 47 const accessToken = await getAccessToken(url, username, password)
47 48
48 await access(program[ 'file' ], constants.F_OK) 49 await access(program['file'], constants.F_OK)
49 50
50 console.log('Uploading %s video...', program[ 'videoName' ]) 51 console.log('Uploading %s video...', program['videoName'])
51 52
52 const videoAttributes = await buildVideoAttributesFromCommander(url, program) 53 const videoAttributes = await buildVideoAttributesFromCommander(url, program)
53 54
54 Object.assign(videoAttributes, { 55 Object.assign(videoAttributes, {
55 fixture: program[ 'file' ], 56 fixture: program['file'],
56 thumbnailfile: program[ 'thumbnail' ], 57 thumbnailfile: program['thumbnail'],
57 previewfile: program[ 'preview' ] 58 previewfile: program['preview']
58 }) 59 })
59 60
60 try { 61 try {
61 await uploadVideo(url, accessToken, videoAttributes) 62 await uploadVideo(url, accessToken, videoAttributes)
62 console.log(`Video ${program[ 'videoName' ]} uploaded.`) 63 console.log(`Video ${program['videoName']} uploaded.`)
63 process.exit(0) 64 process.exit(0)
64 } catch (err) { 65 } catch (err) {
65 console.error(require('util').inspect(err)) 66 console.error(require('util').inspect(err))
diff --git a/server/tools/peertube-watch.ts b/server/tools/peertube-watch.ts
index 9ac1d05f9..5f7d1bb07 100644
--- a/server/tools/peertube-watch.ts
+++ b/server/tools/peertube-watch.ts
@@ -29,16 +29,10 @@ program
29 console.log(' $ peertube watch https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10') 29 console.log(' $ peertube watch https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10')
30 console.log() 30 console.log()
31 }) 31 })
32 .action((url, cmd) => { 32 .action((url, cmd) => run(url, cmd))
33 run(url, cmd)
34 .catch(err => {
35 console.error(err)
36 process.exit(-1)
37 })
38 })
39 .parse(process.argv) 33 .parse(process.argv)
40 34
41async function run (url: string, program: any) { 35function run (url: string, program: any) {
42 if (!url) { 36 if (!url) {
43 console.error('<url> positional argument is required.') 37 console.error('<url> positional argument is required.')
44 process.exit(-1) 38 process.exit(-1)
diff --git a/server/tools/peertube.ts b/server/tools/peertube.ts
index fc85c4210..88dd5f7f6 100644
--- a/server/tools/peertube.ts
+++ b/server/tools/peertube.ts
@@ -1,13 +1,12 @@
1#!/usr/bin/env node 1#!/usr/bin/env node
2 2
3/* eslint-disable no-useless-escape */
4
3import { registerTSPaths } from '../helpers/register-ts-paths' 5import { registerTSPaths } from '../helpers/register-ts-paths'
4registerTSPaths() 6registerTSPaths()
5 7
6import * as program from 'commander' 8import * as program from 'commander'
7import { 9import { getSettings, version } from './cli'
8 version,
9 getSettings
10} from './cli'
11 10
12program 11program
13 .version(version, '-v, --version') 12 .version(version, '-v, --version')
@@ -22,17 +21,19 @@ program
22 .command('watch', 'watch a video in the terminal ✩°。⋆').alias('w') 21 .command('watch', 'watch a video in the terminal ✩°。⋆').alias('w')
23 .command('repl', 'initiate a REPL to access internals') 22 .command('repl', 'initiate a REPL to access internals')
24 .command('plugins [action]', 'manage instance plugins/themes').alias('p') 23 .command('plugins [action]', 'manage instance plugins/themes').alias('p')
24 .command('redundancy [action]', 'manage instance redundancies').alias('r')
25 25
26/* Not Yet Implemented */ 26/* Not Yet Implemented */
27program 27program
28 .command('diagnostic [action]', 28 .command(
29 'like couple therapy, but for your instance', 29 'diagnostic [action]',
30 { noHelp: true } as program.CommandOptions 30 'like couple therapy, but for your instance',
31 ).alias('d') 31 { noHelp: true } as program.CommandOptions
32 ).alias('d')
32 .command('admin', 33 .command('admin',
33 'manage an instance where you have elevated rights', 34 'manage an instance where you have elevated rights',
34 { noHelp: true } as program.CommandOptions 35 { noHelp: true } as program.CommandOptions
35 ).alias('a') 36 ).alias('a')
36 37
37// help on no command 38// help on no command
38if (!process.argv.slice(2).length) { 39if (!process.argv.slice(2).length) {
@@ -81,3 +82,4 @@ getSettings()
81 }) 82 })
82 .parse(process.argv) 83 .parse(process.argv)
83 }) 84 })
85 .catch(err => console.error(err))
diff --git a/server/tools/yarn.lock b/server/tools/yarn.lock
index 28756cbc2..ccd716a51 100644
--- a/server/tools/yarn.lock
+++ b/server/tools/yarn.lock
@@ -347,12 +347,15 @@ chunk-store-stream@^4.0.0:
347 block-stream2 "^2.0.0" 347 block-stream2 "^2.0.0"
348 readable-stream "^3.4.0" 348 readable-stream "^3.4.0"
349 349
350cli-table@^0.3.1: 350cli-table3@^0.5.1:
351 version "0.3.1" 351 version "0.5.1"
352 resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" 352 resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202"
353 integrity sha1-9TsFJmqLGguTSz0IIebi3FkUriM= 353 integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==
354 dependencies: 354 dependencies:
355 colors "1.0.3" 355 object-assign "^4.1.0"
356 string-width "^2.1.1"
357 optionalDependencies:
358 colors "^1.1.2"
356 359
357clivas@^0.2.0: 360clivas@^0.2.0:
358 version "0.2.0" 361 version "0.2.0"
@@ -364,10 +367,10 @@ code-point-at@^1.0.0:
364 resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" 367 resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
365 integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= 368 integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
366 369
367colors@1.0.3: 370colors@^1.1.2:
368 version "1.0.3" 371 version "1.4.0"
369 resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" 372 resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
370 integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= 373 integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
371 374
372common-tags@^1.8.0: 375common-tags@^1.8.0:
373 version "1.8.0" 376 version "1.8.0"
@@ -1609,7 +1612,7 @@ string-width@^1.0.1:
1609 is-fullwidth-code-point "^1.0.0" 1612 is-fullwidth-code-point "^1.0.0"
1610 strip-ansi "^3.0.0" 1613 strip-ansi "^3.0.0"
1611 1614
1612"string-width@^1.0.2 || 2": 1615"string-width@^1.0.2 || 2", string-width@^2.1.1:
1613 version "2.1.1" 1616 version "2.1.1"
1614 resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" 1617 resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
1615 integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== 1618 integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==