aboutsummaryrefslogtreecommitdiffhomepage
path: root/apps
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-07-31 14:34:36 +0200
committerChocobozzz <me@florianbigard.com>2023-08-11 15:02:33 +0200
commit3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch)
treee4510b39bdac9c318fdb4b47018d08f15368b8f0 /apps
parent04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff)
downloadPeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'apps')
-rw-r--r--apps/peertube-cli/.npmignore4
-rw-r--r--apps/peertube-cli/README.md43
-rw-r--r--apps/peertube-cli/package.json19
-rw-r--r--apps/peertube-cli/scripts/build.js27
-rw-r--r--apps/peertube-cli/scripts/watch.js7
-rw-r--r--apps/peertube-cli/src/peertube-auth.ts171
-rw-r--r--apps/peertube-cli/src/peertube-get-access-token.ts39
-rw-r--r--apps/peertube-cli/src/peertube-plugins.ts167
-rw-r--r--apps/peertube-cli/src/peertube-redundancy.ts186
-rw-r--r--apps/peertube-cli/src/peertube-upload.ts167
-rw-r--r--apps/peertube-cli/src/peertube.ts64
-rw-r--r--apps/peertube-cli/src/shared/cli.ts195
-rw-r--r--apps/peertube-cli/src/shared/index.ts1
-rw-r--r--apps/peertube-cli/tsconfig.json15
-rw-r--r--apps/peertube-cli/yarn.lock374
-rw-r--r--apps/peertube-runner/.gitignore3
-rw-r--r--apps/peertube-runner/.npmignore4
-rw-r--r--apps/peertube-runner/README.md43
-rw-r--r--apps/peertube-runner/package.json20
-rw-r--r--apps/peertube-runner/scripts/build.js26
-rw-r--r--apps/peertube-runner/src/peertube-runner.ts91
-rw-r--r--apps/peertube-runner/src/register/index.ts1
-rw-r--r--apps/peertube-runner/src/register/register.ts36
-rw-r--r--apps/peertube-runner/src/server/index.ts1
-rw-r--r--apps/peertube-runner/src/server/process/index.ts2
-rw-r--r--apps/peertube-runner/src/server/process/process.ts34
-rw-r--r--apps/peertube-runner/src/server/process/shared/common.ts106
-rw-r--r--apps/peertube-runner/src/server/process/shared/index.ts3
-rw-r--r--apps/peertube-runner/src/server/process/shared/process-live.ts338
-rw-r--r--apps/peertube-runner/src/server/process/shared/process-studio.ts165
-rw-r--r--apps/peertube-runner/src/server/process/shared/process-vod.ts201
-rw-r--r--apps/peertube-runner/src/server/process/shared/transcoding-logger.ts10
-rw-r--r--apps/peertube-runner/src/server/server.ts307
-rw-r--r--apps/peertube-runner/src/server/shared/index.ts1
-rw-r--r--apps/peertube-runner/src/server/shared/supported-job.ts43
-rw-r--r--apps/peertube-runner/src/shared/config-manager.ts140
-rw-r--r--apps/peertube-runner/src/shared/http.ts67
-rw-r--r--apps/peertube-runner/src/shared/index.ts3
-rw-r--r--apps/peertube-runner/src/shared/ipc/index.ts2
-rw-r--r--apps/peertube-runner/src/shared/ipc/ipc-client.ts88
-rw-r--r--apps/peertube-runner/src/shared/ipc/ipc-server.ts61
-rw-r--r--apps/peertube-runner/src/shared/ipc/shared/index.ts2
-rw-r--r--apps/peertube-runner/src/shared/ipc/shared/ipc-request.model.ts15
-rw-r--r--apps/peertube-runner/src/shared/ipc/shared/ipc-response.model.ts15
-rw-r--r--apps/peertube-runner/src/shared/logger.ts12
-rw-r--r--apps/peertube-runner/tsconfig.json16
-rw-r--r--apps/peertube-runner/yarn.lock528
47 files changed, 3863 insertions, 0 deletions
diff --git a/apps/peertube-cli/.npmignore b/apps/peertube-cli/.npmignore
new file mode 100644
index 000000000..af17b9f32
--- /dev/null
+++ b/apps/peertube-cli/.npmignore
@@ -0,0 +1,4 @@
1src
2meta.json
3tsconfig.json
4scripts
diff --git a/apps/peertube-cli/README.md b/apps/peertube-cli/README.md
new file mode 100644
index 000000000..b5b379090
--- /dev/null
+++ b/apps/peertube-cli/README.md
@@ -0,0 +1,43 @@
1# PeerTube CLI
2
3## Usage
4
5See https://docs.joinpeertube.org/maintain/tools#remote-tools
6
7## Dev
8
9## Install dependencies
10
11```bash
12cd peertube-root
13yarn install --pure-lockfile
14cd apps/peertube-cli && yarn install --pure-lockfile
15```
16
17## Develop
18
19```bash
20cd peertube-root
21npm run dev:peertube-cli
22```
23
24## Build
25
26```bash
27cd peertube-root
28npm run build:peertube-cli
29```
30
31## Run
32
33```bash
34cd peertube-root
35node apps/peertube-cli/dist/peertube-cli.js --help
36```
37
38## Publish on NPM
39
40```bash
41cd peertube-root
42(cd apps/peertube-cli && npm version patch) && npm run build:peertube-cli && (cd apps/peertube-cli && npm publish --access=public)
43```
diff --git a/apps/peertube-cli/package.json b/apps/peertube-cli/package.json
new file mode 100644
index 000000000..a78319be2
--- /dev/null
+++ b/apps/peertube-cli/package.json
@@ -0,0 +1,19 @@
1{
2 "name": "@peertube/peertube-cli",
3 "version": "1.0.1",
4 "type": "module",
5 "main": "dist/peertube.js",
6 "bin": "dist/peertube.js",
7 "engines": {
8 "node": ">=16.x"
9 },
10 "scripts": {},
11 "license": "AGPL-3.0",
12 "private": false,
13 "devDependencies": {
14 "application-config": "^2.0.0",
15 "cli-table3": "^0.6.0",
16 "netrc-parser": "^3.1.6"
17 },
18 "dependencies": {}
19}
diff --git a/apps/peertube-cli/scripts/build.js b/apps/peertube-cli/scripts/build.js
new file mode 100644
index 000000000..a9139acfa
--- /dev/null
+++ b/apps/peertube-cli/scripts/build.js
@@ -0,0 +1,27 @@
1import * as esbuild from 'esbuild'
2import { readFileSync } from 'fs'
3
4const packageJSON = JSON.parse(readFileSync(new URL('../package.json', import.meta.url)))
5
6export const esbuildOptions = {
7 entryPoints: [ './src/peertube.ts' ],
8 bundle: true,
9 platform: 'node',
10 format: 'esm',
11 target: 'node16',
12 external: [
13 './lib-cov/fluent-ffmpeg',
14 'pg-hstore'
15 ],
16 outfile: './dist/peertube.js',
17 banner: {
18 js: `const require = (await import("node:module")).createRequire(import.meta.url);` +
19 `const __filename = (await import("node:url")).fileURLToPath(import.meta.url);` +
20 `const __dirname = (await import("node:path")).dirname(__filename);`
21 },
22 define: {
23 'process.env.PACKAGE_VERSION': `'${packageJSON.version}'`
24 }
25}
26
27await esbuild.build(esbuildOptions)
diff --git a/apps/peertube-cli/scripts/watch.js b/apps/peertube-cli/scripts/watch.js
new file mode 100644
index 000000000..94e57199c
--- /dev/null
+++ b/apps/peertube-cli/scripts/watch.js
@@ -0,0 +1,7 @@
1import * as esbuild from 'esbuild'
2import { esbuildOptions } from './build.js'
3
4const context = await esbuild.context(esbuildOptions)
5
6// Enable watch mode
7await context.watch()
diff --git a/apps/peertube-cli/src/peertube-auth.ts b/apps/peertube-cli/src/peertube-auth.ts
new file mode 100644
index 000000000..1d30207c7
--- /dev/null
+++ b/apps/peertube-cli/src/peertube-auth.ts
@@ -0,0 +1,171 @@
1import CliTable3 from 'cli-table3'
2import prompt from 'prompt'
3import { Command } from '@commander-js/extra-typings'
4import { assignToken, buildServer, getNetrc, getSettings, writeSettings } from './shared/index.js'
5
6export function defineAuthProgram () {
7 const program = new Command()
8 .name('auth')
9 .description('Register your accounts on remote instances to use them with other commands')
10
11 program
12 .command('add')
13 .description('remember your accounts on remote instances for easier use')
14 .option('-u, --url <url>', 'Server url')
15 .option('-U, --username <username>', 'Username')
16 .option('-p, --password <token>', 'Password')
17 .option('--default', 'add the entry as the new default')
18 .action(options => {
19 /* eslint-disable no-import-assign */
20 prompt.override = options
21 prompt.start()
22 prompt.get({
23 properties: {
24 url: {
25 description: 'instance url',
26 conform: value => isURLaPeerTubeInstance(value),
27 message: 'It should be an URL (https://peertube.example.com)',
28 required: true
29 },
30 username: {
31 conform: value => typeof value === 'string' && value.length !== 0,
32 message: 'Name must be only letters, spaces, or dashes',
33 required: true
34 },
35 password: {
36 hidden: true,
37 replace: '*',
38 required: true
39 }
40 }
41 }, async (_, result) => {
42
43 // Check credentials
44 try {
45 // Strip out everything after the domain:port.
46 // See https://github.com/Chocobozzz/PeerTube/issues/3520
47 result.url = stripExtraneousFromPeerTubeUrl(result.url)
48
49 const server = buildServer(result.url)
50 await assignToken(server, result.username, result.password)
51 } catch (err) {
52 console.error(err.message)
53 process.exit(-1)
54 }
55
56 await setInstance(result.url, result.username, result.password, options.default)
57
58 process.exit(0)
59 })
60 })
61
62 program
63 .command('del <url>')
64 .description('Unregisters a remote instance')
65 .action(async url => {
66 await delInstance(url)
67
68 process.exit(0)
69 })
70
71 program
72 .command('list')
73 .description('List registered remote instances')
74 .action(async () => {
75 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
76
77 const table = new CliTable3({
78 head: [ 'instance', 'login' ],
79 colWidths: [ 30, 30 ]
80 }) as any
81
82 settings.remotes.forEach(element => {
83 if (!netrc.machines[element]) return
84
85 table.push([
86 element,
87 netrc.machines[element].login
88 ])
89 })
90
91 console.log(table.toString())
92
93 process.exit(0)
94 })
95
96 program
97 .command('set-default <url>')
98 .description('Set an existing entry as default')
99 .action(async url => {
100 const settings = await getSettings()
101 const instanceExists = settings.remotes.includes(url)
102
103 if (instanceExists) {
104 settings.default = settings.remotes.indexOf(url)
105 await writeSettings(settings)
106
107 process.exit(0)
108 } else {
109 console.log('<url> is not a registered instance.')
110 process.exit(-1)
111 }
112 })
113
114 program.addHelpText('after', '\n\n Examples:\n\n' +
115 ' $ peertube auth add -u https://peertube.cpy.re -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"\n' +
116 ' $ peertube auth add -u https://peertube.cpy.re -U root\n' +
117 ' $ peertube auth list\n' +
118 ' $ peertube auth del https://peertube.cpy.re\n'
119 )
120
121 return program
122}
123
124// ---------------------------------------------------------------------------
125// Private
126// ---------------------------------------------------------------------------
127
128async function delInstance (url: string) {
129 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
130
131 const index = settings.remotes.indexOf(url)
132 settings.remotes.splice(index)
133
134 if (settings.default === index) settings.default = -1
135
136 await writeSettings(settings)
137
138 delete netrc.machines[url]
139
140 await netrc.save()
141}
142
143async function setInstance (url: string, username: string, password: string, isDefault: boolean) {
144 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
145
146 if (settings.remotes.includes(url) === false) {
147 settings.remotes.push(url)
148 }
149
150 if (isDefault || settings.remotes.length === 1) {
151 settings.default = settings.remotes.length - 1
152 }
153
154 await writeSettings(settings)
155
156 netrc.machines[url] = { login: username, password }
157 await netrc.save()
158}
159
160function isURLaPeerTubeInstance (url: string) {
161 return url.startsWith('http://') || url.startsWith('https://')
162}
163
164function stripExtraneousFromPeerTubeUrl (url: string) {
165 // Get everything before the 3rd /.
166 const urlLength = url.includes('/', 8)
167 ? url.indexOf('/', 8)
168 : url.length
169
170 return url.substring(0, urlLength)
171}
diff --git a/apps/peertube-cli/src/peertube-get-access-token.ts b/apps/peertube-cli/src/peertube-get-access-token.ts
new file mode 100644
index 000000000..3e0013182
--- /dev/null
+++ b/apps/peertube-cli/src/peertube-get-access-token.ts
@@ -0,0 +1,39 @@
1import { Command } from '@commander-js/extra-typings'
2import { assignToken, buildServer } from './shared/index.js'
3
4export function defineGetAccessProgram () {
5 const program = new Command()
6 .name('get-access-token')
7 .description('Get a peertube access token')
8 .alias('token')
9
10 program
11 .option('-u, --url <url>', 'Server url')
12 .option('-n, --username <username>', 'Username')
13 .option('-p, --password <token>', 'Password')
14 .action(async options => {
15 try {
16 if (
17 !options.url ||
18 !options.username ||
19 !options.password
20 ) {
21 if (!options.url) console.error('--url field is required.')
22 if (!options.username) console.error('--username field is required.')
23 if (!options.password) console.error('--password field is required.')
24
25 process.exit(-1)
26 }
27
28 const server = buildServer(options.url)
29 await assignToken(server, options.username, options.password)
30
31 console.log(server.accessToken)
32 } catch (err) {
33 console.error('Cannot get access token: ' + err.message)
34 process.exit(-1)
35 }
36 })
37
38 return program
39}
diff --git a/apps/peertube-cli/src/peertube-plugins.ts b/apps/peertube-cli/src/peertube-plugins.ts
new file mode 100644
index 000000000..c9da56266
--- /dev/null
+++ b/apps/peertube-cli/src/peertube-plugins.ts
@@ -0,0 +1,167 @@
1import CliTable3 from 'cli-table3'
2import { isAbsolute } from 'path'
3import { Command } from '@commander-js/extra-typings'
4import { PluginType, PluginType_Type } from '@peertube/peertube-models'
5import { assignToken, buildServer, CommonProgramOptions, getServerCredentials } from './shared/index.js'
6
7export function definePluginsProgram () {
8 const program = new Command()
9
10 program
11 .name('plugins')
12 .description('Manage instance plugins/themes')
13 .alias('p')
14
15 program
16 .command('list')
17 .description('List installed plugins')
18 .option('-u, --url <url>', 'Server url')
19 .option('-U, --username <username>', 'Username')
20 .option('-p, --password <token>', 'Password')
21 .option('-t, --only-themes', 'List themes only')
22 .option('-P, --only-plugins', 'List plugins only')
23 .action(async options => {
24 try {
25 await pluginsListCLI(options)
26 } catch (err) {
27 console.error('Cannot list plugins: ' + err.message)
28 process.exit(-1)
29 }
30 })
31
32 program
33 .command('install')
34 .description('Install a plugin or a theme')
35 .option('-u, --url <url>', 'Server url')
36 .option('-U, --username <username>', 'Username')
37 .option('-p, --password <token>', 'Password')
38 .option('-P --path <path>', 'Install from a path')
39 .option('-n, --npm-name <npmName>', 'Install from npm')
40 .option('--plugin-version <pluginVersion>', 'Specify the plugin version to install (only available when installing from npm)')
41 .action(async options => {
42 try {
43 await installPluginCLI(options)
44 } catch (err) {
45 console.error('Cannot install plugin: ' + err.message)
46 process.exit(-1)
47 }
48 })
49
50 program
51 .command('update')
52 .description('Update a plugin or a theme')
53 .option('-u, --url <url>', 'Server url')
54 .option('-U, --username <username>', 'Username')
55 .option('-p, --password <token>', 'Password')
56 .option('-P --path <path>', 'Update from a path')
57 .option('-n, --npm-name <npmName>', 'Update from npm')
58 .action(async options => {
59 try {
60 await updatePluginCLI(options)
61 } catch (err) {
62 console.error('Cannot update plugin: ' + err.message)
63 process.exit(-1)
64 }
65 })
66
67 program
68 .command('uninstall')
69 .description('Uninstall a plugin or a theme')
70 .option('-u, --url <url>', 'Server url')
71 .option('-U, --username <username>', 'Username')
72 .option('-p, --password <token>', 'Password')
73 .option('-n, --npm-name <npmName>', 'NPM plugin/theme name')
74 .action(async options => {
75 try {
76 await uninstallPluginCLI(options)
77 } catch (err) {
78 console.error('Cannot uninstall plugin: ' + err.message)
79 process.exit(-1)
80 }
81 })
82
83 return program
84}
85
86// ----------------------------------------------------------------------------
87
88async function pluginsListCLI (options: CommonProgramOptions & { onlyThemes?: true, onlyPlugins?: true }) {
89 const { url, username, password } = await getServerCredentials(options)
90 const server = buildServer(url)
91 await assignToken(server, username, password)
92
93 let pluginType: PluginType_Type
94 if (options.onlyThemes) pluginType = PluginType.THEME
95 if (options.onlyPlugins) pluginType = PluginType.PLUGIN
96
97 const { data } = await server.plugins.list({ start: 0, count: 100, sort: 'name', pluginType })
98
99 const table = new CliTable3({
100 head: [ 'name', 'version', 'homepage' ],
101 colWidths: [ 50, 20, 50 ]
102 }) as any
103
104 for (const plugin of data) {
105 const npmName = plugin.type === PluginType.PLUGIN
106 ? 'peertube-plugin-' + plugin.name
107 : 'peertube-theme-' + plugin.name
108
109 table.push([
110 npmName,
111 plugin.version,
112 plugin.homepage
113 ])
114 }
115
116 console.log(table.toString())
117}
118
119async function installPluginCLI (options: CommonProgramOptions & { path?: string, npmName?: string, pluginVersion?: string }) {
120 if (!options.path && !options.npmName) {
121 throw new Error('You need to specify the npm name or the path of the plugin you want to install.')
122 }
123
124 if (options.path && !isAbsolute(options.path)) {
125 throw new Error('Path should be absolute.')
126 }
127
128 const { url, username, password } = await getServerCredentials(options)
129 const server = buildServer(url)
130 await assignToken(server, username, password)
131
132 await server.plugins.install({ npmName: options.npmName, path: options.path, pluginVersion: options.pluginVersion })
133
134 console.log('Plugin installed.')
135}
136
137async function updatePluginCLI (options: CommonProgramOptions & { path?: string, npmName?: string }) {
138 if (!options.path && !options.npmName) {
139 throw new Error('You need to specify the npm name or the path of the plugin you want to update.')
140 }
141
142 if (options.path && !isAbsolute(options.path)) {
143 throw new Error('Path should be absolute.')
144 }
145
146 const { url, username, password } = await getServerCredentials(options)
147 const server = buildServer(url)
148 await assignToken(server, username, password)
149
150 await server.plugins.update({ npmName: options.npmName, path: options.path })
151
152 console.log('Plugin updated.')
153}
154
155async function uninstallPluginCLI (options: CommonProgramOptions & { npmName?: string }) {
156 if (!options.npmName) {
157 throw new Error('You need to specify the npm name of the plugin/theme you want to uninstall.')
158 }
159
160 const { url, username, password } = await getServerCredentials(options)
161 const server = buildServer(url)
162 await assignToken(server, username, password)
163
164 await server.plugins.uninstall({ npmName: options.npmName })
165
166 console.log('Plugin uninstalled.')
167}
diff --git a/apps/peertube-cli/src/peertube-redundancy.ts b/apps/peertube-cli/src/peertube-redundancy.ts
new file mode 100644
index 000000000..56fc6366b
--- /dev/null
+++ b/apps/peertube-cli/src/peertube-redundancy.ts
@@ -0,0 +1,186 @@
1import bytes from 'bytes'
2import CliTable3 from 'cli-table3'
3import { URL } from 'url'
4import { Command } from '@commander-js/extra-typings'
5import { forceNumber, uniqify } from '@peertube/peertube-core-utils'
6import { HttpStatusCode, VideoRedundanciesTarget } from '@peertube/peertube-models'
7import { assignToken, buildServer, CommonProgramOptions, getServerCredentials } from './shared/index.js'
8
9export function defineRedundancyProgram () {
10 const program = new Command()
11 .name('redundancy')
12 .description('Manage instance redundancies')
13 .alias('r')
14
15 program
16 .command('list-remote-redundancies')
17 .description('List remote redundancies on your videos')
18 .option('-u, --url <url>', 'Server url')
19 .option('-U, --username <username>', 'Username')
20 .option('-p, --password <token>', 'Password')
21 .action(async options => {
22 try {
23 await listRedundanciesCLI({ target: 'my-videos', ...options })
24 } catch (err) {
25 console.error('Cannot list remote redundancies: ' + err.message)
26 process.exit(-1)
27 }
28 })
29
30 program
31 .command('list-my-redundancies')
32 .description('List your redundancies of remote videos')
33 .option('-u, --url <url>', 'Server url')
34 .option('-U, --username <username>', 'Username')
35 .option('-p, --password <token>', 'Password')
36 .action(async options => {
37 try {
38 await listRedundanciesCLI({ target: 'remote-videos', ...options })
39 } catch (err) {
40 console.error('Cannot list redundancies: ' + err.message)
41 process.exit(-1)
42 }
43 })
44
45 program
46 .command('add')
47 .description('Duplicate a video in your redundancy system')
48 .option('-u, --url <url>', 'Server url')
49 .option('-U, --username <username>', 'Username')
50 .option('-p, --password <token>', 'Password')
51 .requiredOption('-v, --video <videoId>', 'Video id to duplicate', parseInt)
52 .action(async options => {
53 try {
54 await addRedundancyCLI(options)
55 } catch (err) {
56 console.error('Cannot duplicate video: ' + err.message)
57 process.exit(-1)
58 }
59 })
60
61 program
62 .command('remove')
63 .description('Remove a video from your redundancies')
64 .option('-u, --url <url>', 'Server url')
65 .option('-U, --username <username>', 'Username')
66 .option('-p, --password <token>', 'Password')
67 .requiredOption('-v, --video <videoId>', 'Video id to remove from redundancies', parseInt)
68 .action(async options => {
69 try {
70 await removeRedundancyCLI(options)
71 } catch (err) {
72 console.error('Cannot remove redundancy: ' + err)
73 process.exit(-1)
74 }
75 })
76
77 return program
78}
79
80// ----------------------------------------------------------------------------
81
82async function listRedundanciesCLI (options: CommonProgramOptions & { target: VideoRedundanciesTarget }) {
83 const { target } = options
84
85 const { url, username, password } = await getServerCredentials(options)
86 const server = buildServer(url)
87 await assignToken(server, username, password)
88
89 const { data } = await server.redundancy.listVideos({ start: 0, count: 100, sort: 'name', target })
90
91 const table = new CliTable3({
92 head: [ 'video id', 'video name', 'video url', 'files', 'playlists', 'by instances', 'total size' ]
93 }) as any
94
95 for (const redundancy of data) {
96 const webVideoFiles = redundancy.redundancies.files
97 const streamingPlaylists = redundancy.redundancies.streamingPlaylists
98
99 let totalSize = ''
100 if (target === 'remote-videos') {
101 const tmp = webVideoFiles.concat(streamingPlaylists)
102 .reduce((a, b) => a + b.size, 0)
103
104 // FIXME: don't use external dependency to stringify bytes: we already have the functions in the client
105 totalSize = bytes(tmp)
106 }
107
108 const instances = uniqify(
109 webVideoFiles.concat(streamingPlaylists)
110 .map(r => r.fileUrl)
111 .map(u => new URL(u).host)
112 )
113
114 table.push([
115 redundancy.id.toString(),
116 redundancy.name,
117 redundancy.url,
118 webVideoFiles.length,
119 streamingPlaylists.length,
120 instances.join('\n'),
121 totalSize
122 ])
123 }
124
125 console.log(table.toString())
126}
127
128async function addRedundancyCLI (options: { video: number } & CommonProgramOptions) {
129 const { url, username, password } = await getServerCredentials(options)
130 const server = buildServer(url)
131 await assignToken(server, username, password)
132
133 if (!options.video || isNaN(options.video)) {
134 throw new Error('You need to specify the video id to duplicate and it should be a number.')
135 }
136
137 try {
138 await server.redundancy.addVideo({ videoId: options.video })
139
140 console.log('Video will be duplicated by your instance!')
141 } catch (err) {
142 if (err.message.includes(HttpStatusCode.CONFLICT_409)) {
143 throw new Error('This video is already duplicated by your instance.')
144 }
145
146 if (err.message.includes(HttpStatusCode.NOT_FOUND_404)) {
147 throw new Error('This video id does not exist.')
148 }
149
150 throw err
151 }
152}
153
154async function removeRedundancyCLI (options: CommonProgramOptions & { video: number }) {
155 const { url, username, password } = await getServerCredentials(options)
156 const server = buildServer(url)
157 await assignToken(server, username, password)
158
159 if (!options.video || isNaN(options.video)) {
160 throw new Error('You need to specify the video id to remove from your redundancies')
161 }
162
163 const videoId = forceNumber(options.video)
164
165 const myVideoRedundancies = await server.redundancy.listVideos({ target: 'my-videos' })
166 let videoRedundancy = myVideoRedundancies.data.find(r => videoId === r.id)
167
168 if (!videoRedundancy) {
169 const remoteVideoRedundancies = await server.redundancy.listVideos({ target: 'remote-videos' })
170 videoRedundancy = remoteVideoRedundancies.data.find(r => videoId === r.id)
171 }
172
173 if (!videoRedundancy) {
174 throw new Error('Video redundancy not found.')
175 }
176
177 const ids = videoRedundancy.redundancies.files
178 .concat(videoRedundancy.redundancies.streamingPlaylists)
179 .map(r => r.id)
180
181 for (const id of ids) {
182 await server.redundancy.removeVideo({ redundancyId: id })
183 }
184
185 console.log('Video redundancy removed!')
186}
diff --git a/apps/peertube-cli/src/peertube-upload.ts b/apps/peertube-cli/src/peertube-upload.ts
new file mode 100644
index 000000000..443f8ce1f
--- /dev/null
+++ b/apps/peertube-cli/src/peertube-upload.ts
@@ -0,0 +1,167 @@
1import { access, constants } from 'fs/promises'
2import { isAbsolute } from 'path'
3import { inspect } from 'util'
4import { Command } from '@commander-js/extra-typings'
5import { VideoPrivacy } from '@peertube/peertube-models'
6import { PeerTubeServer } from '@peertube/peertube-server-commands'
7import { assignToken, buildServer, getServerCredentials, listOptions } from './shared/index.js'
8
9type UploadOptions = {
10 url?: string
11 username?: string
12 password?: string
13 thumbnail?: string
14 preview?: string
15 file?: string
16 videoName?: string
17 category?: string
18 licence?: string
19 language?: string
20 tags?: string
21 nsfw?: true
22 videoDescription?: string
23 privacy?: number
24 channelName?: string
25 noCommentsEnabled?: true
26 support?: string
27 noWaitTranscoding?: true
28 noDownloadEnabled?: true
29}
30
31export function defineUploadProgram () {
32 const program = new Command('upload')
33 .description('Upload a video on a PeerTube instance')
34 .alias('up')
35
36 program
37 .option('-u, --url <url>', 'Server url')
38 .option('-U, --username <username>', 'Username')
39 .option('-p, --password <token>', 'Password')
40 .option('-b, --thumbnail <thumbnailPath>', 'Thumbnail path')
41 .option('-v, --preview <previewPath>', 'Preview path')
42 .option('-f, --file <file>', 'Video absolute file path')
43 .option('-n, --video-name <name>', 'Video name')
44 .option('-c, --category <category_number>', 'Category number')
45 .option('-l, --licence <licence_number>', 'Licence number')
46 .option('-L, --language <language_code>', 'Language ISO 639 code (fr or en...)')
47 .option('-t, --tags <tags>', 'Video tags', listOptions)
48 .option('-N, --nsfw', 'Video is Not Safe For Work')
49 .option('-d, --video-description <description>', 'Video description')
50 .option('-P, --privacy <privacy_number>', 'Privacy', parseInt)
51 .option('-C, --channel-name <channel_name>', 'Channel name')
52 .option('--no-comments-enabled', 'Disable video comments')
53 .option('-s, --support <support>', 'Video support text')
54 .option('--no-wait-transcoding', 'Do not wait transcoding before publishing the video')
55 .option('--no-download-enabled', 'Disable video download')
56 .option('-v, --verbose <verbose>', 'Verbosity, from 0/\'error\' to 4/\'debug\'', 'info')
57 .action(async options => {
58 try {
59 const { url, username, password } = await getServerCredentials(options)
60
61 if (!options.videoName || !options.file) {
62 if (!options.videoName) console.error('--video-name is required.')
63 if (!options.file) console.error('--file is required.')
64
65 process.exit(-1)
66 }
67
68 if (isAbsolute(options.file) === false) {
69 console.error('File path should be absolute.')
70 process.exit(-1)
71 }
72
73 await run({ ...options, url, username, password })
74 } catch (err) {
75 console.error('Cannot upload video: ' + err.message)
76 process.exit(-1)
77 }
78 })
79
80 return program
81}
82
83// ---------------------------------------------------------------------------
84// Private
85// ---------------------------------------------------------------------------
86
87async function run (options: UploadOptions) {
88 const { url, username, password } = options
89
90 const server = buildServer(url)
91 await assignToken(server, username, password)
92
93 await access(options.file, constants.F_OK)
94
95 console.log('Uploading %s video...', options.videoName)
96
97 const baseAttributes = await buildVideoAttributesFromCommander(server, options)
98
99 const attributes = {
100 ...baseAttributes,
101
102 fixture: options.file,
103 thumbnailfile: options.thumbnail,
104 previewfile: options.preview
105 }
106
107 try {
108 await server.videos.upload({ attributes })
109 console.log(`Video ${options.videoName} uploaded.`)
110 process.exit(0)
111 } catch (err) {
112 const message = err.message || ''
113 if (message.includes('413')) {
114 console.error('Aborted: user quota is exceeded or video file is too big for this PeerTube instance.')
115 } else {
116 console.error(inspect(err))
117 }
118
119 process.exit(-1)
120 }
121}
122
123async function buildVideoAttributesFromCommander (server: PeerTubeServer, options: UploadOptions, defaultAttributes: any = {}) {
124 const defaultBooleanAttributes = {
125 nsfw: false,
126 commentsEnabled: true,
127 downloadEnabled: true,
128 waitTranscoding: true
129 }
130
131 const booleanAttributes: { [id in keyof typeof defaultBooleanAttributes]: boolean } | {} = {}
132
133 for (const key of Object.keys(defaultBooleanAttributes)) {
134 if (options[key] !== undefined) {
135 booleanAttributes[key] = options[key]
136 } else if (defaultAttributes[key] !== undefined) {
137 booleanAttributes[key] = defaultAttributes[key]
138 } else {
139 booleanAttributes[key] = defaultBooleanAttributes[key]
140 }
141 }
142
143 const videoAttributes = {
144 name: options.videoName || defaultAttributes.name,
145 category: options.category || defaultAttributes.category || undefined,
146 licence: options.licence || defaultAttributes.licence || undefined,
147 language: options.language || defaultAttributes.language || undefined,
148 privacy: options.privacy || defaultAttributes.privacy || VideoPrivacy.PUBLIC,
149 support: options.support || defaultAttributes.support || undefined,
150 description: options.videoDescription || defaultAttributes.description || undefined,
151 tags: options.tags || defaultAttributes.tags || undefined
152 }
153
154 Object.assign(videoAttributes, booleanAttributes)
155
156 if (options.channelName) {
157 const videoChannel = await server.channels.get({ channelName: options.channelName })
158
159 Object.assign(videoAttributes, { channelId: videoChannel.id })
160
161 if (!videoAttributes.support && videoChannel.support) {
162 Object.assign(videoAttributes, { support: videoChannel.support })
163 }
164 }
165
166 return videoAttributes
167}
diff --git a/apps/peertube-cli/src/peertube.ts b/apps/peertube-cli/src/peertube.ts
new file mode 100644
index 000000000..e3565bb1a
--- /dev/null
+++ b/apps/peertube-cli/src/peertube.ts
@@ -0,0 +1,64 @@
1#!/usr/bin/env node
2
3import { Command } from '@commander-js/extra-typings'
4import { defineAuthProgram } from './peertube-auth.js'
5import { defineGetAccessProgram } from './peertube-get-access-token.js'
6import { definePluginsProgram } from './peertube-plugins.js'
7import { defineRedundancyProgram } from './peertube-redundancy.js'
8import { defineUploadProgram } from './peertube-upload.js'
9import { getSettings, version } from './shared/index.js'
10
11const program = new Command()
12
13program
14 .version(version, '-v, --version')
15 .usage('[command] [options]')
16
17program.addCommand(defineAuthProgram())
18program.addCommand(defineUploadProgram())
19program.addCommand(defineRedundancyProgram())
20program.addCommand(definePluginsProgram())
21program.addCommand(defineGetAccessProgram())
22
23// help on no command
24if (!process.argv.slice(2).length) {
25 const logo = 'â–‘Pâ–‘eâ–‘eâ–‘râ–‘Tâ–‘uâ–‘bâ–‘eâ–‘'
26 console.log(`
27 ___/),.._ ` + logo + `
28/' ,. ."'._
29( "' '-.__"-._ ,-
30\\'='='), "\\ -._-"-. -"/
31 / ""/"\\,_\\,__"" _" /,-
32 / / -" _/"/
33 / | ._\\\\ |\\ |_.".-" /
34 / | __\\)|)|),/|_." _,."
35 / \\_." " ") | ).-""---''--
36 ( "/.""7__-""''
37 | " ."._--._
38 \\ \\ (_ __ "" ".,_
39 \\.,. \\ "" -"".-"
40 ".,_, (",_-,,,-".-
41 "'-,\\_ __,-"
42 ",)" ")
43 /"\\-"
44 ,"\\/
45 _,.__/"\\/_ (the CLI for red chocobos)
46 / \\) "./, ".
47 --/---"---" "-) )---- by Chocobozzz et al.\n`)
48}
49
50getSettings()
51 .then(settings => {
52 const state = (settings.default === undefined || settings.default === -1)
53 ? 'no instance selected, commands will require explicit arguments'
54 : 'instance ' + settings.remotes[settings.default] + ' selected'
55
56 program
57 .addHelpText('after', '\n\n State: ' + state + '\n\n' +
58 ' Examples:\n\n' +
59 ' $ peertube auth add -u "PEERTUBE_URL" -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"\n' +
60 ' $ peertube up <videoFile>\n'
61 )
62 .parse(process.argv)
63 })
64 .catch(err => console.error(err))
diff --git a/apps/peertube-cli/src/shared/cli.ts b/apps/peertube-cli/src/shared/cli.ts
new file mode 100644
index 000000000..080eb8237
--- /dev/null
+++ b/apps/peertube-cli/src/shared/cli.ts
@@ -0,0 +1,195 @@
1import applicationConfig from 'application-config'
2import { Netrc } from 'netrc-parser'
3import { join } from 'path'
4import { createLogger, format, transports } from 'winston'
5import { UserRole } from '@peertube/peertube-models'
6import { getAppNumber, isTestInstance, root } from '@peertube/peertube-node-utils'
7import { PeerTubeServer } from '@peertube/peertube-server-commands'
8
9export type CommonProgramOptions = {
10 url?: string
11 username?: string
12 password?: string
13}
14
15let configName = 'PeerTube/CLI'
16if (isTestInstance()) configName += `-${getAppNumber()}`
17
18const config = applicationConfig(configName)
19
20const version: string = process.env.PACKAGE_VERSION
21
22async function getAdminTokenOrDie (server: PeerTubeServer, username: string, password: string) {
23 const token = await server.login.getAccessToken(username, password)
24 const me = await server.users.getMyInfo({ token })
25
26 if (me.role.id !== UserRole.ADMINISTRATOR) {
27 console.error('You must be an administrator.')
28 process.exit(-1)
29 }
30
31 return token
32}
33
34interface Settings {
35 remotes: any[]
36 default: number
37}
38
39async function getSettings () {
40 const defaultSettings: Settings = {
41 remotes: [],
42 default: -1
43 }
44
45 const data = await config.read() as Promise<Settings>
46
47 return Object.keys(data).length === 0
48 ? defaultSettings
49 : data
50}
51
52async function getNetrc () {
53 const netrc = isTestInstance()
54 ? new Netrc(join(root(import.meta.url), 'test' + getAppNumber(), 'netrc'))
55 : new Netrc()
56
57 await netrc.load()
58
59 return netrc
60}
61
62function writeSettings (settings: Settings) {
63 return config.write(settings)
64}
65
66function deleteSettings () {
67 return config.trash()
68}
69
70function getRemoteObjectOrDie (
71 options: CommonProgramOptions,
72 settings: Settings,
73 netrc: Netrc
74): { url: string, username: string, password: string } {
75
76 function exitIfNoOptions (optionNames: string[], errorPrefix: string = '') {
77 let exit = false
78
79 for (const key of optionNames) {
80 if (!options[key]) {
81 if (exit === false && errorPrefix) console.error(errorPrefix)
82
83 console.error(`--${key} field is required`)
84 exit = true
85 }
86 }
87
88 if (exit) process.exit(-1)
89 }
90
91 // If username or password are specified, both are mandatory
92 if (options.username || options.password) {
93 exitIfNoOptions([ 'username', 'password' ])
94 }
95
96 // If no available machines, url, username and password args are mandatory
97 if (Object.keys(netrc.machines).length === 0) {
98 exitIfNoOptions([ 'url', 'username', 'password' ], 'No account found in netrc')
99 }
100
101 if (settings.remotes.length === 0 || settings.default === -1) {
102 exitIfNoOptions([ 'url' ], 'No default instance found')
103 }
104
105 let url: string = options.url
106 let username: string = options.username
107 let password: string = options.password
108
109 if (!url && settings.default !== -1) url = settings.remotes[settings.default]
110
111 const machine = netrc.machines[url]
112 if ((!username || !password) && !machine) {
113 console.error('Cannot find existing configuration for %s.', url)
114 process.exit(-1)
115 }
116
117 if (!username && machine) username = machine.login
118 if (!password && machine) password = machine.password
119
120 return { url, username, password }
121}
122
123function listOptions (val: any) {
124 return val.split(',')
125}
126
127function getServerCredentials (options: CommonProgramOptions) {
128 return Promise.all([ getSettings(), getNetrc() ])
129 .then(([ settings, netrc ]) => {
130 return getRemoteObjectOrDie(options, settings, netrc)
131 })
132}
133
134function buildServer (url: string) {
135 return new PeerTubeServer({ url })
136}
137
138async function assignToken (server: PeerTubeServer, username: string, password: string) {
139 const bodyClient = await server.login.getClient()
140 const client = { id: bodyClient.client_id, secret: bodyClient.client_secret }
141
142 const body = await server.login.login({ client, user: { username, password } })
143
144 server.accessToken = body.access_token
145}
146
147function getLogger (logLevel = 'info') {
148 const logLevels = {
149 0: 0,
150 error: 0,
151 1: 1,
152 warn: 1,
153 2: 2,
154 info: 2,
155 3: 3,
156 verbose: 3,
157 4: 4,
158 debug: 4
159 }
160
161 const logger = createLogger({
162 levels: logLevels,
163 format: format.combine(
164 format.splat(),
165 format.simple()
166 ),
167 transports: [
168 new (transports.Console)({
169 level: logLevel
170 })
171 ]
172 })
173
174 return logger
175}
176
177// ---------------------------------------------------------------------------
178
179export {
180 version,
181 getLogger,
182 getSettings,
183 getNetrc,
184 getRemoteObjectOrDie,
185 writeSettings,
186 deleteSettings,
187
188 getServerCredentials,
189
190 listOptions,
191
192 getAdminTokenOrDie,
193 buildServer,
194 assignToken
195}
diff --git a/apps/peertube-cli/src/shared/index.ts b/apps/peertube-cli/src/shared/index.ts
new file mode 100644
index 000000000..a1fc9470b
--- /dev/null
+++ b/apps/peertube-cli/src/shared/index.ts
@@ -0,0 +1 @@
export * from './cli.js'
diff --git a/apps/peertube-cli/tsconfig.json b/apps/peertube-cli/tsconfig.json
new file mode 100644
index 000000000..636bdb95a
--- /dev/null
+++ b/apps/peertube-cli/tsconfig.json
@@ -0,0 +1,15 @@
1{
2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "baseUrl": "./",
5 "outDir": "./dist",
6 "rootDir": "src",
7 "tsBuildInfoFile": "./dist/.tsbuildinfo"
8 },
9 "references": [
10 { "path": "../../packages/core-utils" },
11 { "path": "../../packages/models" },
12 { "path": "../../packages/node-utils" },
13 { "path": "../../packages/server-commands" }
14 ]
15}
diff --git a/apps/peertube-cli/yarn.lock b/apps/peertube-cli/yarn.lock
new file mode 100644
index 000000000..76b36ee73
--- /dev/null
+++ b/apps/peertube-cli/yarn.lock
@@ -0,0 +1,374 @@
1# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2# yarn lockfile v1
3
4
5"@babel/code-frame@^7.0.0":
6 version "7.22.10"
7 resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.10.tgz#1c20e612b768fefa75f6e90d6ecb86329247f0a3"
8 integrity sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==
9 dependencies:
10 "@babel/highlight" "^7.22.10"
11 chalk "^2.4.2"
12
13"@babel/helper-validator-identifier@^7.22.5":
14 version "7.22.5"
15 resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193"
16 integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==
17
18"@babel/highlight@^7.22.10":
19 version "7.22.10"
20 resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7"
21 integrity sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==
22 dependencies:
23 "@babel/helper-validator-identifier" "^7.22.5"
24 chalk "^2.4.2"
25 js-tokens "^4.0.0"
26
27"@colors/colors@1.5.0":
28 version "1.5.0"
29 resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
30 integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
31
32ansi-regex@^5.0.1:
33 version "5.0.1"
34 resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
35 integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
36
37ansi-styles@^3.2.1:
38 version "3.2.1"
39 resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
40 integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
41 dependencies:
42 color-convert "^1.9.0"
43
44application-config-path@^0.1.0:
45 version "0.1.1"
46 resolved "https://registry.yarnpkg.com/application-config-path/-/application-config-path-0.1.1.tgz#8b5ac64ff6afdd9bd70ce69f6f64b6998f5f756e"
47 integrity sha512-zy9cHePtMP0YhwG+CfHm0bgwdnga2X3gZexpdCwEj//dpb+TKajtiC8REEUJUSq6Ab4f9cgNy2l8ObXzCXFkEw==
48
49application-config@^2.0.0:
50 version "2.0.0"
51 resolved "https://registry.yarnpkg.com/application-config/-/application-config-2.0.0.tgz#15b4d54d61c0c082f9802227e3e85de876b47747"
52 integrity sha512-NC5/0guSZK3/UgUDfCk/riByXzqz0owL1L3r63JPSBzYk5QALrp3bLxbsR7qeSfvYfFmAhnp3dbqYsW3U9MpZQ==
53 dependencies:
54 application-config-path "^0.1.0"
55 load-json-file "^6.2.0"
56 write-json-file "^4.2.0"
57
58chalk@^2.4.2:
59 version "2.4.2"
60 resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
61 integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
62 dependencies:
63 ansi-styles "^3.2.1"
64 escape-string-regexp "^1.0.5"
65 supports-color "^5.3.0"
66
67cli-table3@^0.6.0:
68 version "0.6.3"
69 resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2"
70 integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==
71 dependencies:
72 string-width "^4.2.0"
73 optionalDependencies:
74 "@colors/colors" "1.5.0"
75
76color-convert@^1.9.0:
77 version "1.9.3"
78 resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
79 integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
80 dependencies:
81 color-name "1.1.3"
82
83color-name@1.1.3:
84 version "1.1.3"
85 resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
86 integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
87
88cross-spawn@^6.0.0:
89 version "6.0.5"
90 resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
91 integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
92 dependencies:
93 nice-try "^1.0.4"
94 path-key "^2.0.1"
95 semver "^5.5.0"
96 shebang-command "^1.2.0"
97 which "^1.2.9"
98
99debug@^3.1.0:
100 version "3.2.7"
101 resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
102 integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
103 dependencies:
104 ms "^2.1.1"
105
106detect-indent@^6.0.0:
107 version "6.1.0"
108 resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
109 integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==
110
111emoji-regex@^8.0.0:
112 version "8.0.0"
113 resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
114 integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
115
116error-ex@^1.3.1:
117 version "1.3.2"
118 resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
119 integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
120 dependencies:
121 is-arrayish "^0.2.1"
122
123escape-string-regexp@^1.0.5:
124 version "1.0.5"
125 resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
126 integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
127
128execa@^0.10.0:
129 version "0.10.0"
130 resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50"
131 integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==
132 dependencies:
133 cross-spawn "^6.0.0"
134 get-stream "^3.0.0"
135 is-stream "^1.1.0"
136 npm-run-path "^2.0.0"
137 p-finally "^1.0.0"
138 signal-exit "^3.0.0"
139 strip-eof "^1.0.0"
140
141get-stream@^3.0.0:
142 version "3.0.0"
143 resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
144 integrity sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==
145
146graceful-fs@^4.1.15:
147 version "4.2.11"
148 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
149 integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
150
151has-flag@^3.0.0:
152 version "3.0.0"
153 resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
154 integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
155
156imurmurhash@^0.1.4:
157 version "0.1.4"
158 resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
159 integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
160
161is-arrayish@^0.2.1:
162 version "0.2.1"
163 resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
164 integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
165
166is-fullwidth-code-point@^3.0.0:
167 version "3.0.0"
168 resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
169 integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
170
171is-plain-obj@^2.0.0:
172 version "2.1.0"
173 resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
174 integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
175
176is-stream@^1.1.0:
177 version "1.1.0"
178 resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
179 integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==
180
181is-typedarray@^1.0.0:
182 version "1.0.0"
183 resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
184 integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
185
186isexe@^2.0.0:
187 version "2.0.0"
188 resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
189 integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
190
191js-tokens@^4.0.0:
192 version "4.0.0"
193 resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
194 integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
195
196json-parse-even-better-errors@^2.3.0:
197 version "2.3.1"
198 resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
199 integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
200
201lines-and-columns@^1.1.6:
202 version "1.2.4"
203 resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
204 integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
205
206load-json-file@^6.2.0:
207 version "6.2.0"
208 resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1"
209 integrity sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==
210 dependencies:
211 graceful-fs "^4.1.15"
212 parse-json "^5.0.0"
213 strip-bom "^4.0.0"
214 type-fest "^0.6.0"
215
216make-dir@^3.0.0:
217 version "3.1.0"
218 resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
219 integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
220 dependencies:
221 semver "^6.0.0"
222
223ms@^2.1.1:
224 version "2.1.3"
225 resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
226 integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
227
228netrc-parser@^3.1.6:
229 version "3.1.6"
230 resolved "https://registry.yarnpkg.com/netrc-parser/-/netrc-parser-3.1.6.tgz#7243c9ec850b8e805b9bdc7eae7b1450d4a96e72"
231 integrity sha512-lY+fmkqSwntAAjfP63jB4z5p5WbuZwyMCD3pInT7dpHU/Gc6Vv90SAC6A0aNiqaRGHiuZFBtiwu+pu8W/Eyotw==
232 dependencies:
233 debug "^3.1.0"
234 execa "^0.10.0"
235
236nice-try@^1.0.4:
237 version "1.0.5"
238 resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
239 integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
240
241npm-run-path@^2.0.0:
242 version "2.0.2"
243 resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
244 integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==
245 dependencies:
246 path-key "^2.0.0"
247
248p-finally@^1.0.0:
249 version "1.0.0"
250 resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
251 integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
252
253parse-json@^5.0.0:
254 version "5.2.0"
255 resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
256 integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
257 dependencies:
258 "@babel/code-frame" "^7.0.0"
259 error-ex "^1.3.1"
260 json-parse-even-better-errors "^2.3.0"
261 lines-and-columns "^1.1.6"
262
263path-key@^2.0.0, path-key@^2.0.1:
264 version "2.0.1"
265 resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
266 integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==
267
268semver@^5.5.0:
269 version "5.7.2"
270 resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
271 integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
272
273semver@^6.0.0:
274 version "6.3.1"
275 resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
276 integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
277
278shebang-command@^1.2.0:
279 version "1.2.0"
280 resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
281 integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==
282 dependencies:
283 shebang-regex "^1.0.0"
284
285shebang-regex@^1.0.0:
286 version "1.0.0"
287 resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
288 integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==
289
290signal-exit@^3.0.0, signal-exit@^3.0.2:
291 version "3.0.7"
292 resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
293 integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
294
295sort-keys@^4.0.0:
296 version "4.2.0"
297 resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-4.2.0.tgz#6b7638cee42c506fff8c1cecde7376d21315be18"
298 integrity sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg==
299 dependencies:
300 is-plain-obj "^2.0.0"
301
302string-width@^4.2.0:
303 version "4.2.3"
304 resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
305 integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
306 dependencies:
307 emoji-regex "^8.0.0"
308 is-fullwidth-code-point "^3.0.0"
309 strip-ansi "^6.0.1"
310
311strip-ansi@^6.0.1:
312 version "6.0.1"
313 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
314 integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
315 dependencies:
316 ansi-regex "^5.0.1"
317
318strip-bom@^4.0.0:
319 version "4.0.0"
320 resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
321 integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
322
323strip-eof@^1.0.0:
324 version "1.0.0"
325 resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
326 integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==
327
328supports-color@^5.3.0:
329 version "5.5.0"
330 resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
331 integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
332 dependencies:
333 has-flag "^3.0.0"
334
335type-fest@^0.6.0:
336 version "0.6.0"
337 resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
338 integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
339
340typedarray-to-buffer@^3.1.5:
341 version "3.1.5"
342 resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
343 integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
344 dependencies:
345 is-typedarray "^1.0.0"
346
347which@^1.2.9:
348 version "1.3.1"
349 resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
350 integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
351 dependencies:
352 isexe "^2.0.0"
353
354write-file-atomic@^3.0.0:
355 version "3.0.3"
356 resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
357 integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
358 dependencies:
359 imurmurhash "^0.1.4"
360 is-typedarray "^1.0.0"
361 signal-exit "^3.0.2"
362 typedarray-to-buffer "^3.1.5"
363
364write-json-file@^4.2.0:
365 version "4.3.0"
366 resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-4.3.0.tgz#908493d6fd23225344af324016e4ca8f702dd12d"
367 integrity sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ==
368 dependencies:
369 detect-indent "^6.0.0"
370 graceful-fs "^4.1.15"
371 is-plain-obj "^2.0.0"
372 make-dir "^3.0.0"
373 sort-keys "^4.0.0"
374 write-file-atomic "^3.0.0"
diff --git a/apps/peertube-runner/.gitignore b/apps/peertube-runner/.gitignore
new file mode 100644
index 000000000..6426ab063
--- /dev/null
+++ b/apps/peertube-runner/.gitignore
@@ -0,0 +1,3 @@
1node_modules
2dist
3meta.json
diff --git a/apps/peertube-runner/.npmignore b/apps/peertube-runner/.npmignore
new file mode 100644
index 000000000..af17b9f32
--- /dev/null
+++ b/apps/peertube-runner/.npmignore
@@ -0,0 +1,4 @@
1src
2meta.json
3tsconfig.json
4scripts
diff --git a/apps/peertube-runner/README.md b/apps/peertube-runner/README.md
new file mode 100644
index 000000000..37760f867
--- /dev/null
+++ b/apps/peertube-runner/README.md
@@ -0,0 +1,43 @@
1# PeerTube runner
2
3Runner program to execute jobs (transcoding...) of remote PeerTube instances.
4
5Commands below has to be run at the root of PeerTube git repository.
6
7## Dev
8
9### Install dependencies
10
11```bash
12cd peertube-root
13yarn install --pure-lockfile
14cd apps/peertube-runner && yarn install --pure-lockfile
15```
16
17### Develop
18
19```bash
20cd peertube-root
21npm run dev:peertube-runner
22```
23
24### Build
25
26```bash
27cd peertube-root
28npm run build:peertube-runner
29```
30
31### Run
32
33```bash
34cd peertube-root
35node apps/peertube-runner/dist/peertube-runner.js --help
36```
37
38### Publish on NPM
39
40```bash
41cd peertube-root
42(cd apps/peertube-runner && npm version patch) && npm run build:peertube-runner && (cd apps/peertube-runner && npm publish --access=public)
43```
diff --git a/apps/peertube-runner/package.json b/apps/peertube-runner/package.json
new file mode 100644
index 000000000..1dca15451
--- /dev/null
+++ b/apps/peertube-runner/package.json
@@ -0,0 +1,20 @@
1{
2 "name": "@peertube/peertube-runner",
3 "version": "0.0.5",
4 "type": "module",
5 "main": "dist/peertube-runner.js",
6 "bin": "dist/peertube-runner.js",
7 "engines": {
8 "node": ">=16.x"
9 },
10 "license": "AGPL-3.0",
11 "dependencies": {},
12 "devDependencies": {
13 "@commander-js/extra-typings": "^10.0.3",
14 "@iarna/toml": "^2.2.5",
15 "env-paths": "^3.0.0",
16 "net-ipc": "^2.0.1",
17 "pino": "^8.11.0",
18 "pino-pretty": "^10.0.0"
19 }
20}
diff --git a/apps/peertube-runner/scripts/build.js b/apps/peertube-runner/scripts/build.js
new file mode 100644
index 000000000..f54ca35f3
--- /dev/null
+++ b/apps/peertube-runner/scripts/build.js
@@ -0,0 +1,26 @@
1import * as esbuild from 'esbuild'
2
3const packageJSON = JSON.parse(readFileSync(new URL('../package.json', import.meta.url)))
4
5export const esbuildOptions = {
6 entryPoints: [ './src/peertube-runner.ts' ],
7 bundle: true,
8 platform: 'node',
9 format: 'esm',
10 target: 'node16',
11 external: [
12 './lib-cov/fluent-ffmpeg',
13 'pg-hstore'
14 ],
15 outfile: './dist/peertube-runner.js',
16 banner: {
17 js: `const require = (await import("node:module")).createRequire(import.meta.url);` +
18 `const __filename = (await import("node:url")).fileURLToPath(import.meta.url);` +
19 `const __dirname = (await import("node:path")).dirname(__filename);`
20 },
21 define: {
22 'process.env.PACKAGE_VERSION': `'${packageJSON.version}'`
23 }
24}
25
26await esbuild.build(esbuildOptions)
diff --git a/apps/peertube-runner/src/peertube-runner.ts b/apps/peertube-runner/src/peertube-runner.ts
new file mode 100644
index 000000000..67ca0e0ac
--- /dev/null
+++ b/apps/peertube-runner/src/peertube-runner.ts
@@ -0,0 +1,91 @@
1#!/usr/bin/env node
2
3import { Command, InvalidArgumentError } from '@commander-js/extra-typings'
4import { listRegistered, registerRunner, unregisterRunner } from './register/index.js'
5import { RunnerServer } from './server/index.js'
6import { ConfigManager, logger } from './shared/index.js'
7
8const program = new Command()
9 .version(process.env.PACKAGE_VERSION)
10 .option(
11 '--id <id>',
12 'Runner server id, so you can run multiple PeerTube server runners with different configurations on the same machine',
13 'default'
14 )
15 .option('--verbose', 'Run in verbose mode')
16 .hook('preAction', thisCommand => {
17 const options = thisCommand.opts()
18
19 ConfigManager.Instance.init(options.id)
20
21 if (options.verbose === true) {
22 logger.level = 'debug'
23 }
24 })
25
26program.command('server')
27 .description('Run in server mode, to execute remote jobs of registered PeerTube instances')
28 .action(async () => {
29 try {
30 await RunnerServer.Instance.run()
31 } catch (err) {
32 logger.error(err, 'Cannot run PeerTube runner as server mode')
33 process.exit(-1)
34 }
35 })
36
37program.command('register')
38 .description('Register a new PeerTube instance to process runner jobs')
39 .requiredOption('--url <url>', 'PeerTube instance URL', parseUrl)
40 .requiredOption('--registration-token <token>', 'Runner registration token (can be found in PeerTube instance administration')
41 .requiredOption('--runner-name <name>', 'Runner name')
42 .option('--runner-description <description>', 'Runner description')
43 .action(async options => {
44 try {
45 await registerRunner(options)
46 } catch (err) {
47 console.error('Cannot register this PeerTube runner.')
48 console.error(err)
49 process.exit(-1)
50 }
51 })
52
53program.command('unregister')
54 .description('Unregister the runner from PeerTube instance')
55 .requiredOption('--url <url>', 'PeerTube instance URL', parseUrl)
56 .requiredOption('--runner-name <name>', 'Runner name')
57 .action(async options => {
58 try {
59 await unregisterRunner(options)
60 } catch (err) {
61 console.error('Cannot unregister this PeerTube runner.')
62 console.error(err)
63 process.exit(-1)
64 }
65 })
66
67program.command('list-registered')
68 .description('List registered PeerTube instances')
69 .action(async () => {
70 try {
71 await listRegistered()
72 } catch (err) {
73 console.error('Cannot list registered PeerTube instances.')
74 console.error(err)
75 process.exit(-1)
76 }
77 })
78
79program.parse()
80
81// ---------------------------------------------------------------------------
82// Private
83// ---------------------------------------------------------------------------
84
85function parseUrl (url: string) {
86 if (url.startsWith('http://') !== true && url.startsWith('https://') !== true) {
87 throw new InvalidArgumentError('URL should start with a http:// or https://')
88 }
89
90 return url
91}
diff --git a/apps/peertube-runner/src/register/index.ts b/apps/peertube-runner/src/register/index.ts
new file mode 100644
index 000000000..a7d6cf457
--- /dev/null
+++ b/apps/peertube-runner/src/register/index.ts
@@ -0,0 +1 @@
export * from './register.js'
diff --git a/apps/peertube-runner/src/register/register.ts b/apps/peertube-runner/src/register/register.ts
new file mode 100644
index 000000000..e8af21661
--- /dev/null
+++ b/apps/peertube-runner/src/register/register.ts
@@ -0,0 +1,36 @@
1import { IPCClient } from '../shared/ipc/index.js'
2
3export async function registerRunner (options: {
4 url: string
5 registrationToken: string
6 runnerName: string
7 runnerDescription?: string
8}) {
9 const client = new IPCClient()
10 await client.run()
11
12 await client.askRegister(options)
13
14 client.stop()
15}
16
17export async function unregisterRunner (options: {
18 url: string
19 runnerName: string
20}) {
21 const client = new IPCClient()
22 await client.run()
23
24 await client.askUnregister(options)
25
26 client.stop()
27}
28
29export async function listRegistered () {
30 const client = new IPCClient()
31 await client.run()
32
33 await client.askListRegistered()
34
35 client.stop()
36}
diff --git a/apps/peertube-runner/src/server/index.ts b/apps/peertube-runner/src/server/index.ts
new file mode 100644
index 000000000..e56cda526
--- /dev/null
+++ b/apps/peertube-runner/src/server/index.ts
@@ -0,0 +1 @@
export * from './server.js'
diff --git a/apps/peertube-runner/src/server/process/index.ts b/apps/peertube-runner/src/server/process/index.ts
new file mode 100644
index 000000000..64a7b00fc
--- /dev/null
+++ b/apps/peertube-runner/src/server/process/index.ts
@@ -0,0 +1,2 @@
1export * from './shared/index.js'
2export * from './process.js'
diff --git a/apps/peertube-runner/src/server/process/process.ts b/apps/peertube-runner/src/server/process/process.ts
new file mode 100644
index 000000000..e8a1d7c28
--- /dev/null
+++ b/apps/peertube-runner/src/server/process/process.ts
@@ -0,0 +1,34 @@
1import {
2 RunnerJobLiveRTMPHLSTranscodingPayload,
3 RunnerJobStudioTranscodingPayload,
4 RunnerJobVODAudioMergeTranscodingPayload,
5 RunnerJobVODHLSTranscodingPayload,
6 RunnerJobVODWebVideoTranscodingPayload
7} from '@peertube/peertube-models'
8import { logger } from '../../shared/index.js'
9import { processAudioMergeTranscoding, processHLSTranscoding, ProcessOptions, processWebVideoTranscoding } from './shared/index.js'
10import { ProcessLiveRTMPHLSTranscoding } from './shared/process-live.js'
11import { processStudioTranscoding } from './shared/process-studio.js'
12
13export async function processJob (options: ProcessOptions) {
14 const { server, job } = options
15
16 logger.info(`[${server.url}] Processing job of type ${job.type}: ${job.uuid}`, { payload: job.payload })
17
18 if (job.type === 'vod-audio-merge-transcoding') {
19 await processAudioMergeTranscoding(options as ProcessOptions<RunnerJobVODAudioMergeTranscodingPayload>)
20 } else if (job.type === 'vod-web-video-transcoding') {
21 await processWebVideoTranscoding(options as ProcessOptions<RunnerJobVODWebVideoTranscodingPayload>)
22 } else if (job.type === 'vod-hls-transcoding') {
23 await processHLSTranscoding(options as ProcessOptions<RunnerJobVODHLSTranscodingPayload>)
24 } else if (job.type === 'live-rtmp-hls-transcoding') {
25 await new ProcessLiveRTMPHLSTranscoding(options as ProcessOptions<RunnerJobLiveRTMPHLSTranscodingPayload>).process()
26 } else if (job.type === 'video-studio-transcoding') {
27 await processStudioTranscoding(options as ProcessOptions<RunnerJobStudioTranscodingPayload>)
28 } else {
29 logger.error(`Unknown job ${job.type} to process`)
30 return
31 }
32
33 logger.info(`[${server.url}] Finished processing job of type ${job.type}: ${job.uuid}`)
34}
diff --git a/apps/peertube-runner/src/server/process/shared/common.ts b/apps/peertube-runner/src/server/process/shared/common.ts
new file mode 100644
index 000000000..09241d93b
--- /dev/null
+++ b/apps/peertube-runner/src/server/process/shared/common.ts
@@ -0,0 +1,106 @@
1import { remove } from 'fs-extra/esm'
2import { join } from 'path'
3import { FFmpegEdition, FFmpegLive, FFmpegVOD, getDefaultAvailableEncoders, getDefaultEncodersToTry } from '@peertube/peertube-ffmpeg'
4import { RunnerJob, RunnerJobPayload } from '@peertube/peertube-models'
5import { buildUUID } from '@peertube/peertube-node-utils'
6import { PeerTubeServer } from '@peertube/peertube-server-commands'
7import { ConfigManager, downloadFile, logger } from '../../../shared/index.js'
8import { getTranscodingLogger } from './transcoding-logger.js'
9
10export type JobWithToken <T extends RunnerJobPayload = RunnerJobPayload> = RunnerJob<T> & { jobToken: string }
11
12export type ProcessOptions <T extends RunnerJobPayload = RunnerJobPayload> = {
13 server: PeerTubeServer
14 job: JobWithToken<T>
15 runnerToken: string
16}
17
18export async function downloadInputFile (options: {
19 url: string
20 job: JobWithToken
21 runnerToken: string
22}) {
23 const { url, job, runnerToken } = options
24 const destination = join(ConfigManager.Instance.getTranscodingDirectory(), buildUUID())
25
26 try {
27 await downloadFile({ url, jobToken: job.jobToken, runnerToken, destination })
28 } catch (err) {
29 remove(destination)
30 .catch(err => logger.error({ err }, `Cannot remove ${destination}`))
31
32 throw err
33 }
34
35 return destination
36}
37
38export function scheduleTranscodingProgress (options: {
39 server: PeerTubeServer
40 runnerToken: string
41 job: JobWithToken
42 progressGetter: () => number
43}) {
44 const { job, server, progressGetter, runnerToken } = options
45
46 const updateInterval = ConfigManager.Instance.isTestInstance()
47 ? 500
48 : 60000
49
50 const update = () => {
51 server.runnerJobs.update({ jobToken: job.jobToken, jobUUID: job.uuid, runnerToken, progress: progressGetter() })
52 .catch(err => logger.error({ err }, 'Cannot send job progress'))
53 }
54
55 const interval = setInterval(() => {
56 update()
57 }, updateInterval)
58
59 update()
60
61 return interval
62}
63
64// ---------------------------------------------------------------------------
65
66export function buildFFmpegVOD (options: {
67 onJobProgress: (progress: number) => void
68}) {
69 const { onJobProgress } = options
70
71 return new FFmpegVOD({
72 ...getCommonFFmpegOptions(),
73
74 updateJobProgress: arg => {
75 const progress = arg < 0 || arg > 100
76 ? undefined
77 : arg
78
79 onJobProgress(progress)
80 }
81 })
82}
83
84export function buildFFmpegLive () {
85 return new FFmpegLive(getCommonFFmpegOptions())
86}
87
88export function buildFFmpegEdition () {
89 return new FFmpegEdition(getCommonFFmpegOptions())
90}
91
92function getCommonFFmpegOptions () {
93 const config = ConfigManager.Instance.getConfig()
94
95 return {
96 niceness: config.ffmpeg.nice,
97 threads: config.ffmpeg.threads,
98 tmpDirectory: ConfigManager.Instance.getTranscodingDirectory(),
99 profile: 'default',
100 availableEncoders: {
101 available: getDefaultAvailableEncoders(),
102 encodersToTry: getDefaultEncodersToTry()
103 },
104 logger: getTranscodingLogger()
105 }
106}
diff --git a/apps/peertube-runner/src/server/process/shared/index.ts b/apps/peertube-runner/src/server/process/shared/index.ts
new file mode 100644
index 000000000..638bf127f
--- /dev/null
+++ b/apps/peertube-runner/src/server/process/shared/index.ts
@@ -0,0 +1,3 @@
1export * from './common.js'
2export * from './process-vod.js'
3export * from './transcoding-logger.js'
diff --git a/apps/peertube-runner/src/server/process/shared/process-live.ts b/apps/peertube-runner/src/server/process/shared/process-live.ts
new file mode 100644
index 000000000..0dc4e5b13
--- /dev/null
+++ b/apps/peertube-runner/src/server/process/shared/process-live.ts
@@ -0,0 +1,338 @@
1import { FSWatcher, watch } from 'chokidar'
2import { FfmpegCommand } from 'fluent-ffmpeg'
3import { ensureDir, remove } from 'fs-extra/esm'
4import { basename, join } from 'path'
5import { wait } from '@peertube/peertube-core-utils'
6import { ffprobePromise, getVideoStreamBitrate, getVideoStreamDimensionsInfo, hasAudioStream } from '@peertube/peertube-ffmpeg'
7import {
8 LiveRTMPHLSTranscodingSuccess,
9 LiveRTMPHLSTranscodingUpdatePayload,
10 PeerTubeProblemDocument,
11 RunnerJobLiveRTMPHLSTranscodingPayload,
12 ServerErrorCode
13} from '@peertube/peertube-models'
14import { buildUUID } from '@peertube/peertube-node-utils'
15import { ConfigManager } from '../../../shared/config-manager.js'
16import { logger } from '../../../shared/index.js'
17import { buildFFmpegLive, ProcessOptions } from './common.js'
18
19export class ProcessLiveRTMPHLSTranscoding {
20
21 private readonly outputPath: string
22 private readonly fsWatchers: FSWatcher[] = []
23
24 // Playlist name -> chunks
25 private readonly pendingChunksPerPlaylist = new Map<string, string[]>()
26
27 private readonly playlistsCreated = new Set<string>()
28 private allPlaylistsCreated = false
29
30 private ffmpegCommand: FfmpegCommand
31
32 private ended = false
33 private errored = false
34
35 constructor (private readonly options: ProcessOptions<RunnerJobLiveRTMPHLSTranscodingPayload>) {
36 this.outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), buildUUID())
37
38 logger.debug(`Using ${this.outputPath} to process live rtmp hls transcoding job ${options.job.uuid}`)
39 }
40
41 process () {
42 const job = this.options.job
43 const payload = job.payload
44
45 return new Promise<void>(async (res, rej) => {
46 try {
47 await ensureDir(this.outputPath)
48
49 logger.info(`Probing ${payload.input.rtmpUrl}`)
50 const probe = await ffprobePromise(payload.input.rtmpUrl)
51 logger.info({ probe }, `Probed ${payload.input.rtmpUrl}`)
52
53 const hasAudio = await hasAudioStream(payload.input.rtmpUrl, probe)
54 const bitrate = await getVideoStreamBitrate(payload.input.rtmpUrl, probe)
55 const { ratio } = await getVideoStreamDimensionsInfo(payload.input.rtmpUrl, probe)
56
57 const m3u8Watcher = watch(this.outputPath + '/*.m3u8')
58 this.fsWatchers.push(m3u8Watcher)
59
60 const tsWatcher = watch(this.outputPath + '/*.ts')
61 this.fsWatchers.push(tsWatcher)
62
63 m3u8Watcher.on('change', p => {
64 logger.debug(`${p} m3u8 playlist changed`)
65 })
66
67 m3u8Watcher.on('add', p => {
68 this.playlistsCreated.add(p)
69
70 if (this.playlistsCreated.size === this.options.job.payload.output.toTranscode.length + 1) {
71 this.allPlaylistsCreated = true
72 logger.info('All m3u8 playlists are created.')
73 }
74 })
75
76 tsWatcher.on('add', async p => {
77 try {
78 await this.sendPendingChunks()
79 } catch (err) {
80 this.onUpdateError({ err, rej, res })
81 }
82
83 const playlistName = this.getPlaylistIdFromTS(p)
84
85 const pendingChunks = this.pendingChunksPerPlaylist.get(playlistName) || []
86 pendingChunks.push(p)
87
88 this.pendingChunksPerPlaylist.set(playlistName, pendingChunks)
89 })
90
91 tsWatcher.on('unlink', p => {
92 this.sendDeletedChunkUpdate(p)
93 .catch(err => this.onUpdateError({ err, rej, res }))
94 })
95
96 this.ffmpegCommand = await buildFFmpegLive().getLiveTranscodingCommand({
97 inputUrl: payload.input.rtmpUrl,
98
99 outPath: this.outputPath,
100 masterPlaylistName: 'master.m3u8',
101
102 segmentListSize: payload.output.segmentListSize,
103 segmentDuration: payload.output.segmentDuration,
104
105 toTranscode: payload.output.toTranscode,
106
107 bitrate,
108 ratio,
109
110 hasAudio
111 })
112
113 logger.info(`Running live transcoding for ${payload.input.rtmpUrl}`)
114
115 this.ffmpegCommand.on('error', (err, stdout, stderr) => {
116 this.onFFmpegError({ err, stdout, stderr })
117
118 res()
119 })
120
121 this.ffmpegCommand.on('end', () => {
122 this.onFFmpegEnded()
123 .catch(err => logger.error({ err }, 'Error in FFmpeg end handler'))
124
125 res()
126 })
127
128 this.ffmpegCommand.run()
129 } catch (err) {
130 rej(err)
131 }
132 })
133 }
134
135 // ---------------------------------------------------------------------------
136
137 private onUpdateError (options: {
138 err: Error
139 res: () => void
140 rej: (reason?: any) => void
141 }) {
142 const { err, res, rej } = options
143
144 if (this.errored) return
145 if (this.ended) return
146
147 this.errored = true
148
149 this.ffmpegCommand.kill('SIGINT')
150
151 const type = ((err as any).res?.body as PeerTubeProblemDocument)?.code
152 if (type === ServerErrorCode.RUNNER_JOB_NOT_IN_PROCESSING_STATE) {
153 logger.info({ err }, 'Stopping transcoding as the job is not in processing state anymore')
154
155 res()
156 } else {
157 logger.error({ err }, 'Cannot send update after added/deleted chunk, stopping live transcoding')
158
159 this.sendError(err)
160 .catch(subErr => logger.error({ err: subErr }, 'Cannot send error'))
161
162 rej(err)
163 }
164
165 this.cleanup()
166 }
167
168 // ---------------------------------------------------------------------------
169
170 private onFFmpegError (options: {
171 err: any
172 stdout: string
173 stderr: string
174 }) {
175 const { err, stdout, stderr } = options
176
177 // Don't care that we killed the ffmpeg process
178 if (err?.message?.includes('Exiting normally')) return
179 if (this.errored) return
180 if (this.ended) return
181
182 this.errored = true
183
184 logger.error({ err, stdout, stderr }, 'FFmpeg transcoding error.')
185
186 this.sendError(err)
187 .catch(subErr => logger.error({ err: subErr }, 'Cannot send error'))
188
189 this.cleanup()
190 }
191
192 private async sendError (err: Error) {
193 await this.options.server.runnerJobs.error({
194 jobToken: this.options.job.jobToken,
195 jobUUID: this.options.job.uuid,
196 runnerToken: this.options.runnerToken,
197 message: err.message
198 })
199 }
200
201 // ---------------------------------------------------------------------------
202
203 private async onFFmpegEnded () {
204 if (this.ended) return
205
206 this.ended = true
207 logger.info('FFmpeg ended, sending success to server')
208
209 // Wait last ffmpeg chunks generation
210 await wait(1500)
211
212 this.sendSuccess()
213 .catch(err => logger.error({ err }, 'Cannot send success'))
214
215 this.cleanup()
216 }
217
218 private async sendSuccess () {
219 const successBody: LiveRTMPHLSTranscodingSuccess = {}
220
221 await this.options.server.runnerJobs.success({
222 jobToken: this.options.job.jobToken,
223 jobUUID: this.options.job.uuid,
224 runnerToken: this.options.runnerToken,
225 payload: successBody
226 })
227 }
228
229 // ---------------------------------------------------------------------------
230
231 private sendDeletedChunkUpdate (deletedChunk: string): Promise<any> {
232 if (this.ended) return Promise.resolve()
233
234 logger.debug(`Sending removed live chunk ${deletedChunk} update`)
235
236 const videoChunkFilename = basename(deletedChunk)
237
238 let payload: LiveRTMPHLSTranscodingUpdatePayload = {
239 type: 'remove-chunk',
240 videoChunkFilename
241 }
242
243 if (this.allPlaylistsCreated) {
244 const playlistName = this.getPlaylistName(videoChunkFilename)
245
246 payload = {
247 ...payload,
248 masterPlaylistFile: join(this.outputPath, 'master.m3u8'),
249 resolutionPlaylistFilename: playlistName,
250 resolutionPlaylistFile: join(this.outputPath, playlistName)
251 }
252 }
253
254 return this.updateWithRetry(payload)
255 }
256
257 private async sendPendingChunks (): Promise<any> {
258 if (this.ended) return Promise.resolve()
259
260 const promises: Promise<any>[] = []
261
262 for (const playlist of this.pendingChunksPerPlaylist.keys()) {
263 for (const chunk of this.pendingChunksPerPlaylist.get(playlist)) {
264 logger.debug(`Sending added live chunk ${chunk} update`)
265
266 const videoChunkFilename = basename(chunk)
267
268 let payload: LiveRTMPHLSTranscodingUpdatePayload = {
269 type: 'add-chunk',
270 videoChunkFilename,
271 videoChunkFile: chunk
272 }
273
274 if (this.allPlaylistsCreated) {
275 const playlistName = this.getPlaylistName(videoChunkFilename)
276
277 payload = {
278 ...payload,
279 masterPlaylistFile: join(this.outputPath, 'master.m3u8'),
280 resolutionPlaylistFilename: playlistName,
281 resolutionPlaylistFile: join(this.outputPath, playlistName)
282 }
283 }
284
285 promises.push(this.updateWithRetry(payload))
286 }
287
288 this.pendingChunksPerPlaylist.set(playlist, [])
289 }
290
291 await Promise.all(promises)
292 }
293
294 private async updateWithRetry (payload: LiveRTMPHLSTranscodingUpdatePayload, currentTry = 1): Promise<any> {
295 if (this.ended || this.errored) return
296
297 try {
298 await this.options.server.runnerJobs.update({
299 jobToken: this.options.job.jobToken,
300 jobUUID: this.options.job.uuid,
301 runnerToken: this.options.runnerToken,
302 payload
303 })
304 } catch (err) {
305 if (currentTry >= 3) throw err
306 if ((err.res?.body as PeerTubeProblemDocument)?.code === ServerErrorCode.RUNNER_JOB_NOT_IN_PROCESSING_STATE) throw err
307
308 logger.warn({ err }, 'Will retry update after error')
309 await wait(250)
310
311 return this.updateWithRetry(payload, currentTry + 1)
312 }
313 }
314
315 private getPlaylistName (videoChunkFilename: string) {
316 return `${videoChunkFilename.split('-')[0]}.m3u8`
317 }
318
319 private getPlaylistIdFromTS (segmentPath: string) {
320 const playlistIdMatcher = /^([\d+])-/
321
322 return basename(segmentPath).match(playlistIdMatcher)[1]
323 }
324
325 // ---------------------------------------------------------------------------
326
327 private cleanup () {
328 logger.debug(`Cleaning up job ${this.options.job.uuid}`)
329
330 for (const fsWatcher of this.fsWatchers) {
331 fsWatcher.close()
332 .catch(err => logger.error({ err }, 'Cannot close watcher'))
333 }
334
335 remove(this.outputPath)
336 .catch(err => logger.error({ err }, `Cannot remove ${this.outputPath}`))
337 }
338}
diff --git a/apps/peertube-runner/src/server/process/shared/process-studio.ts b/apps/peertube-runner/src/server/process/shared/process-studio.ts
new file mode 100644
index 000000000..11b7b7d9a
--- /dev/null
+++ b/apps/peertube-runner/src/server/process/shared/process-studio.ts
@@ -0,0 +1,165 @@
1import { remove } from 'fs-extra/esm'
2import { join } from 'path'
3import { pick } from '@peertube/peertube-core-utils'
4import {
5 RunnerJobStudioTranscodingPayload,
6 VideoStudioTask,
7 VideoStudioTaskCutPayload,
8 VideoStudioTaskIntroPayload,
9 VideoStudioTaskOutroPayload,
10 VideoStudioTaskPayload,
11 VideoStudioTaskWatermarkPayload,
12 VideoStudioTranscodingSuccess
13} from '@peertube/peertube-models'
14import { buildUUID } from '@peertube/peertube-node-utils'
15import { ConfigManager } from '../../../shared/config-manager.js'
16import { logger } from '../../../shared/index.js'
17import { buildFFmpegEdition, downloadInputFile, JobWithToken, ProcessOptions, scheduleTranscodingProgress } from './common.js'
18
19export async function processStudioTranscoding (options: ProcessOptions<RunnerJobStudioTranscodingPayload>) {
20 const { server, job, runnerToken } = options
21 const payload = job.payload
22
23 let inputPath: string
24 let outputPath: string
25 let tmpInputFilePath: string
26
27 let tasksProgress = 0
28
29 const updateProgressInterval = scheduleTranscodingProgress({
30 job,
31 server,
32 runnerToken,
33 progressGetter: () => tasksProgress
34 })
35
36 try {
37 logger.info(`Downloading input file ${payload.input.videoFileUrl} for job ${job.jobToken}`)
38
39 inputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job })
40 tmpInputFilePath = inputPath
41
42 logger.info(`Input file ${payload.input.videoFileUrl} downloaded for job ${job.jobToken}. Running studio transcoding tasks.`)
43
44 for (const task of payload.tasks) {
45 const outputFilename = 'output-edition-' + buildUUID() + '.mp4'
46 outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), outputFilename)
47
48 await processTask({
49 inputPath: tmpInputFilePath,
50 outputPath,
51 task,
52 job,
53 runnerToken
54 })
55
56 if (tmpInputFilePath) await remove(tmpInputFilePath)
57
58 // For the next iteration
59 tmpInputFilePath = outputPath
60
61 tasksProgress += Math.floor(100 / payload.tasks.length)
62 }
63
64 const successBody: VideoStudioTranscodingSuccess = {
65 videoFile: outputPath
66 }
67
68 await server.runnerJobs.success({
69 jobToken: job.jobToken,
70 jobUUID: job.uuid,
71 runnerToken,
72 payload: successBody
73 })
74 } finally {
75 if (tmpInputFilePath) await remove(tmpInputFilePath)
76 if (outputPath) await remove(outputPath)
77 if (updateProgressInterval) clearInterval(updateProgressInterval)
78 }
79}
80
81// ---------------------------------------------------------------------------
82// Private
83// ---------------------------------------------------------------------------
84
85type TaskProcessorOptions <T extends VideoStudioTaskPayload = VideoStudioTaskPayload> = {
86 inputPath: string
87 outputPath: string
88 task: T
89 runnerToken: string
90 job: JobWithToken
91}
92
93const taskProcessors: { [id in VideoStudioTask['name']]: (options: TaskProcessorOptions) => Promise<any> } = {
94 'add-intro': processAddIntroOutro,
95 'add-outro': processAddIntroOutro,
96 'cut': processCut,
97 'add-watermark': processAddWatermark
98}
99
100async function processTask (options: TaskProcessorOptions) {
101 const { task } = options
102
103 const processor = taskProcessors[options.task.name]
104 if (!process) throw new Error('Unknown task ' + task.name)
105
106 return processor(options)
107}
108
109async function processAddIntroOutro (options: TaskProcessorOptions<VideoStudioTaskIntroPayload | VideoStudioTaskOutroPayload>) {
110 const { inputPath, task, runnerToken, job } = options
111
112 logger.debug('Adding intro/outro to ' + inputPath)
113
114 const introOutroPath = await downloadInputFile({ url: task.options.file, runnerToken, job })
115
116 try {
117 await buildFFmpegEdition().addIntroOutro({
118 ...pick(options, [ 'inputPath', 'outputPath' ]),
119
120 introOutroPath,
121 type: task.name === 'add-intro'
122 ? 'intro'
123 : 'outro'
124 })
125 } finally {
126 await remove(introOutroPath)
127 }
128}
129
130function processCut (options: TaskProcessorOptions<VideoStudioTaskCutPayload>) {
131 const { inputPath, task } = options
132
133 logger.debug(`Cutting ${inputPath}`)
134
135 return buildFFmpegEdition().cutVideo({
136 ...pick(options, [ 'inputPath', 'outputPath' ]),
137
138 start: task.options.start,
139 end: task.options.end
140 })
141}
142
143async function processAddWatermark (options: TaskProcessorOptions<VideoStudioTaskWatermarkPayload>) {
144 const { inputPath, task, runnerToken, job } = options
145
146 logger.debug('Adding watermark to ' + inputPath)
147
148 const watermarkPath = await downloadInputFile({ url: task.options.file, runnerToken, job })
149
150 try {
151 await buildFFmpegEdition().addWatermark({
152 ...pick(options, [ 'inputPath', 'outputPath' ]),
153
154 watermarkPath,
155
156 videoFilters: {
157 watermarkSizeRatio: task.options.watermarkSizeRatio,
158 horitonzalMarginRatio: task.options.horitonzalMarginRatio,
159 verticalMarginRatio: task.options.verticalMarginRatio
160 }
161 })
162 } finally {
163 await remove(watermarkPath)
164 }
165}
diff --git a/apps/peertube-runner/src/server/process/shared/process-vod.ts b/apps/peertube-runner/src/server/process/shared/process-vod.ts
new file mode 100644
index 000000000..fe1715ca9
--- /dev/null
+++ b/apps/peertube-runner/src/server/process/shared/process-vod.ts
@@ -0,0 +1,201 @@
1import { remove } from 'fs-extra/esm'
2import { join } from 'path'
3import {
4 RunnerJobVODAudioMergeTranscodingPayload,
5 RunnerJobVODHLSTranscodingPayload,
6 RunnerJobVODWebVideoTranscodingPayload,
7 VODAudioMergeTranscodingSuccess,
8 VODHLSTranscodingSuccess,
9 VODWebVideoTranscodingSuccess
10} from '@peertube/peertube-models'
11import { buildUUID } from '@peertube/peertube-node-utils'
12import { ConfigManager } from '../../../shared/config-manager.js'
13import { logger } from '../../../shared/index.js'
14import { buildFFmpegVOD, downloadInputFile, ProcessOptions, scheduleTranscodingProgress } from './common.js'
15
16export async function processWebVideoTranscoding (options: ProcessOptions<RunnerJobVODWebVideoTranscodingPayload>) {
17 const { server, job, runnerToken } = options
18
19 const payload = job.payload
20
21 let ffmpegProgress: number
22 let inputPath: string
23
24 const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `output-${buildUUID()}.mp4`)
25
26 const updateProgressInterval = scheduleTranscodingProgress({
27 job,
28 server,
29 runnerToken,
30 progressGetter: () => ffmpegProgress
31 })
32
33 try {
34 logger.info(`Downloading input file ${payload.input.videoFileUrl} for web video transcoding job ${job.jobToken}`)
35
36 inputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job })
37
38 logger.info(`Downloaded input file ${payload.input.videoFileUrl} for job ${job.jobToken}. Running web video transcoding.`)
39
40 const ffmpegVod = buildFFmpegVOD({
41 onJobProgress: progress => { ffmpegProgress = progress }
42 })
43
44 await ffmpegVod.transcode({
45 type: 'video',
46
47 inputPath,
48
49 outputPath,
50
51 inputFileMutexReleaser: () => {},
52
53 resolution: payload.output.resolution,
54 fps: payload.output.fps
55 })
56
57 const successBody: VODWebVideoTranscodingSuccess = {
58 videoFile: outputPath
59 }
60
61 await server.runnerJobs.success({
62 jobToken: job.jobToken,
63 jobUUID: job.uuid,
64 runnerToken,
65 payload: successBody
66 })
67 } finally {
68 if (inputPath) await remove(inputPath)
69 if (outputPath) await remove(outputPath)
70 if (updateProgressInterval) clearInterval(updateProgressInterval)
71 }
72}
73
74export async function processHLSTranscoding (options: ProcessOptions<RunnerJobVODHLSTranscodingPayload>) {
75 const { server, job, runnerToken } = options
76 const payload = job.payload
77
78 let ffmpegProgress: number
79 let inputPath: string
80
81 const uuid = buildUUID()
82 const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `${uuid}-${payload.output.resolution}.m3u8`)
83 const videoFilename = `${uuid}-${payload.output.resolution}-fragmented.mp4`
84 const videoPath = join(join(ConfigManager.Instance.getTranscodingDirectory(), videoFilename))
85
86 const updateProgressInterval = scheduleTranscodingProgress({
87 job,
88 server,
89 runnerToken,
90 progressGetter: () => ffmpegProgress
91 })
92
93 try {
94 logger.info(`Downloading input file ${payload.input.videoFileUrl} for HLS transcoding job ${job.jobToken}`)
95
96 inputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job })
97
98 logger.info(`Downloaded input file ${payload.input.videoFileUrl} for job ${job.jobToken}. Running HLS transcoding.`)
99
100 const ffmpegVod = buildFFmpegVOD({
101 onJobProgress: progress => { ffmpegProgress = progress }
102 })
103
104 await ffmpegVod.transcode({
105 type: 'hls',
106 copyCodecs: false,
107 inputPath,
108 hlsPlaylist: { videoFilename },
109 outputPath,
110
111 inputFileMutexReleaser: () => {},
112
113 resolution: payload.output.resolution,
114 fps: payload.output.fps
115 })
116
117 const successBody: VODHLSTranscodingSuccess = {
118 resolutionPlaylistFile: outputPath,
119 videoFile: videoPath
120 }
121
122 await server.runnerJobs.success({
123 jobToken: job.jobToken,
124 jobUUID: job.uuid,
125 runnerToken,
126 payload: successBody
127 })
128 } finally {
129 if (inputPath) await remove(inputPath)
130 if (outputPath) await remove(outputPath)
131 if (videoPath) await remove(videoPath)
132 if (updateProgressInterval) clearInterval(updateProgressInterval)
133 }
134}
135
136export async function processAudioMergeTranscoding (options: ProcessOptions<RunnerJobVODAudioMergeTranscodingPayload>) {
137 const { server, job, runnerToken } = options
138 const payload = job.payload
139
140 let ffmpegProgress: number
141 let audioPath: string
142 let inputPath: string
143
144 const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `output-${buildUUID()}.mp4`)
145
146 const updateProgressInterval = scheduleTranscodingProgress({
147 job,
148 server,
149 runnerToken,
150 progressGetter: () => ffmpegProgress
151 })
152
153 try {
154 logger.info(
155 `Downloading input files ${payload.input.audioFileUrl} and ${payload.input.previewFileUrl} ` +
156 `for audio merge transcoding job ${job.jobToken}`
157 )
158
159 audioPath = await downloadInputFile({ url: payload.input.audioFileUrl, runnerToken, job })
160 inputPath = await downloadInputFile({ url: payload.input.previewFileUrl, runnerToken, job })
161
162 logger.info(
163 `Downloaded input files ${payload.input.audioFileUrl} and ${payload.input.previewFileUrl} ` +
164 `for job ${job.jobToken}. Running audio merge transcoding.`
165 )
166
167 const ffmpegVod = buildFFmpegVOD({
168 onJobProgress: progress => { ffmpegProgress = progress }
169 })
170
171 await ffmpegVod.transcode({
172 type: 'merge-audio',
173
174 audioPath,
175 inputPath,
176
177 outputPath,
178
179 inputFileMutexReleaser: () => {},
180
181 resolution: payload.output.resolution,
182 fps: payload.output.fps
183 })
184
185 const successBody: VODAudioMergeTranscodingSuccess = {
186 videoFile: outputPath
187 }
188
189 await server.runnerJobs.success({
190 jobToken: job.jobToken,
191 jobUUID: job.uuid,
192 runnerToken,
193 payload: successBody
194 })
195 } finally {
196 if (audioPath) await remove(audioPath)
197 if (inputPath) await remove(inputPath)
198 if (outputPath) await remove(outputPath)
199 if (updateProgressInterval) clearInterval(updateProgressInterval)
200 }
201}
diff --git a/apps/peertube-runner/src/server/process/shared/transcoding-logger.ts b/apps/peertube-runner/src/server/process/shared/transcoding-logger.ts
new file mode 100644
index 000000000..041dd62eb
--- /dev/null
+++ b/apps/peertube-runner/src/server/process/shared/transcoding-logger.ts
@@ -0,0 +1,10 @@
1import { logger } from '../../../shared/index.js'
2
3export function getTranscodingLogger () {
4 return {
5 info: logger.info.bind(logger),
6 debug: logger.debug.bind(logger),
7 warn: logger.warn.bind(logger),
8 error: logger.error.bind(logger)
9 }
10}
diff --git a/apps/peertube-runner/src/server/server.ts b/apps/peertube-runner/src/server/server.ts
new file mode 100644
index 000000000..825e3f297
--- /dev/null
+++ b/apps/peertube-runner/src/server/server.ts
@@ -0,0 +1,307 @@
1import { ensureDir, remove } from 'fs-extra/esm'
2import { readdir } from 'fs/promises'
3import { join } from 'path'
4import { io, Socket } from 'socket.io-client'
5import { pick, shuffle, wait } from '@peertube/peertube-core-utils'
6import { PeerTubeProblemDocument, ServerErrorCode } from '@peertube/peertube-models'
7import { PeerTubeServer as PeerTubeServerCommand } from '@peertube/peertube-server-commands'
8import { ConfigManager } from '../shared/index.js'
9import { IPCServer } from '../shared/ipc/index.js'
10import { logger } from '../shared/logger.js'
11import { JobWithToken, processJob } from './process/index.js'
12import { isJobSupported } from './shared/index.js'
13
14type PeerTubeServer = PeerTubeServerCommand & {
15 runnerToken: string
16 runnerName: string
17 runnerDescription?: string
18}
19
20export class RunnerServer {
21 private static instance: RunnerServer
22
23 private servers: PeerTubeServer[] = []
24 private processingJobs: { job: JobWithToken, server: PeerTubeServer }[] = []
25
26 private checkingAvailableJobs = false
27
28 private cleaningUp = false
29
30 private readonly sockets = new Map<PeerTubeServer, Socket>()
31
32 private constructor () {}
33
34 async run () {
35 logger.info('Running PeerTube runner in server mode')
36
37 await ConfigManager.Instance.load()
38
39 for (const registered of ConfigManager.Instance.getConfig().registeredInstances) {
40 const serverCommand = new PeerTubeServerCommand({ url: registered.url })
41
42 this.loadServer(Object.assign(serverCommand, registered))
43
44 logger.info(`Loading registered instance ${registered.url}`)
45 }
46
47 // Run IPC
48 const ipcServer = new IPCServer()
49 try {
50 await ipcServer.run(this)
51 } catch (err) {
52 logger.error('Cannot start local socket for IPC communication', err)
53 process.exit(-1)
54 }
55
56 // Cleanup on exit
57 for (const code of [ 'SIGINT', 'SIGUSR1', 'SIGUSR2', 'uncaughtException' ]) {
58 process.on(code, async (err, origin) => {
59 if (code === 'uncaughtException') {
60 logger.error({ err, origin }, 'uncaughtException')
61 }
62
63 await this.onExit()
64 })
65 }
66
67 // Process jobs
68 await ensureDir(ConfigManager.Instance.getTranscodingDirectory())
69 await this.cleanupTMP()
70
71 logger.info(`Using ${ConfigManager.Instance.getTranscodingDirectory()} for transcoding directory`)
72
73 await this.checkAvailableJobs()
74 }
75
76 // ---------------------------------------------------------------------------
77
78 async registerRunner (options: {
79 url: string
80 registrationToken: string
81 runnerName: string
82 runnerDescription?: string
83 }) {
84 const { url, registrationToken, runnerName, runnerDescription } = options
85
86 logger.info(`Registering runner ${runnerName} on ${url}...`)
87
88 const serverCommand = new PeerTubeServerCommand({ url })
89 const { runnerToken } = await serverCommand.runners.register({ name: runnerName, description: runnerDescription, registrationToken })
90
91 const server: PeerTubeServer = Object.assign(serverCommand, {
92 runnerToken,
93 runnerName,
94 runnerDescription
95 })
96
97 this.loadServer(server)
98 await this.saveRegisteredInstancesInConf()
99
100 logger.info(`Registered runner ${runnerName} on ${url}`)
101
102 await this.checkAvailableJobs()
103 }
104
105 private loadServer (server: PeerTubeServer) {
106 this.servers.push(server)
107
108 const url = server.url + '/runners'
109 const socket = io(url, {
110 auth: {
111 runnerToken: server.runnerToken
112 },
113 transports: [ 'websocket' ]
114 })
115
116 socket.on('connect_error', err => logger.warn({ err }, `Cannot connect to ${url} socket`))
117 socket.on('connect', () => logger.info(`Connected to ${url} socket`))
118 socket.on('available-jobs', () => this.checkAvailableJobs())
119
120 this.sockets.set(server, socket)
121 }
122
123 async unregisterRunner (options: {
124 url: string
125 runnerName: string
126 }) {
127 const { url, runnerName } = options
128
129 const server = this.servers.find(s => s.url === url && s.runnerName === runnerName)
130 if (!server) {
131 logger.error(`Unknown server ${url} - ${runnerName} to unregister`)
132 return
133 }
134
135 logger.info(`Unregistering runner ${runnerName} on ${url}...`)
136
137 try {
138 await server.runners.unregister({ runnerToken: server.runnerToken })
139 } catch (err) {
140 logger.error({ err }, `Cannot unregister runner ${runnerName} on ${url}`)
141 }
142
143 this.unloadServer(server)
144 await this.saveRegisteredInstancesInConf()
145
146 logger.info(`Unregistered runner ${runnerName} on ${url}`)
147 }
148
149 private unloadServer (server: PeerTubeServer) {
150 this.servers = this.servers.filter(s => s !== server)
151
152 const socket = this.sockets.get(server)
153 socket.disconnect()
154
155 this.sockets.delete(server)
156 }
157
158 listRegistered () {
159 return {
160 servers: this.servers.map(s => {
161 return {
162 url: s.url,
163 runnerName: s.runnerName,
164 runnerDescription: s.runnerDescription
165 }
166 })
167 }
168 }
169
170 // ---------------------------------------------------------------------------
171
172 private async checkAvailableJobs () {
173 if (this.checkingAvailableJobs) return
174
175 this.checkingAvailableJobs = true
176
177 let hadAvailableJob = false
178
179 for (const server of shuffle([ ...this.servers ])) {
180 try {
181 logger.info('Checking available jobs on ' + server.url)
182
183 const job = await this.requestJob(server)
184 if (!job) continue
185
186 hadAvailableJob = true
187
188 await this.tryToExecuteJobAsync(server, job)
189 } catch (err) {
190 const code = (err.res?.body as PeerTubeProblemDocument)?.code
191
192 if (code === ServerErrorCode.RUNNER_JOB_NOT_IN_PROCESSING_STATE) {
193 logger.debug({ err }, 'Runner job is not in processing state anymore, retry later')
194 return
195 }
196
197 if (code === ServerErrorCode.UNKNOWN_RUNNER_TOKEN) {
198 logger.error({ err }, `Unregistering ${server.url} as the runner token ${server.runnerToken} is invalid`)
199
200 await this.unregisterRunner({ url: server.url, runnerName: server.runnerName })
201 return
202 }
203
204 logger.error({ err }, `Cannot request/accept job on ${server.url} for runner ${server.runnerName}`)
205 }
206 }
207
208 this.checkingAvailableJobs = false
209
210 if (hadAvailableJob && this.canProcessMoreJobs()) {
211 await wait(2500)
212
213 this.checkAvailableJobs()
214 .catch(err => logger.error({ err }, 'Cannot check more available jobs'))
215 }
216 }
217
218 private async requestJob (server: PeerTubeServer) {
219 logger.debug(`Requesting jobs on ${server.url} for runner ${server.runnerName}`)
220
221 const { availableJobs } = await server.runnerJobs.request({ runnerToken: server.runnerToken })
222
223 const filtered = availableJobs.filter(j => isJobSupported(j))
224
225 if (filtered.length === 0) {
226 logger.debug(`No job available on ${server.url} for runner ${server.runnerName}`)
227 return undefined
228 }
229
230 return filtered[0]
231 }
232
233 private async tryToExecuteJobAsync (server: PeerTubeServer, jobToAccept: { uuid: string }) {
234 if (!this.canProcessMoreJobs()) return
235
236 const { job } = await server.runnerJobs.accept({ runnerToken: server.runnerToken, jobUUID: jobToAccept.uuid })
237
238 const processingJob = { job, server }
239 this.processingJobs.push(processingJob)
240
241 processJob({ server, job, runnerToken: server.runnerToken })
242 .catch(err => {
243 logger.error({ err }, 'Cannot process job')
244
245 server.runnerJobs.error({ jobToken: job.jobToken, jobUUID: job.uuid, runnerToken: server.runnerToken, message: err.message })
246 .catch(err2 => logger.error({ err: err2 }, 'Cannot abort job after error'))
247 })
248 .finally(() => {
249 this.processingJobs = this.processingJobs.filter(p => p !== processingJob)
250
251 return this.checkAvailableJobs()
252 })
253 }
254
255 // ---------------------------------------------------------------------------
256
257 private saveRegisteredInstancesInConf () {
258 const data = this.servers.map(s => {
259 return pick(s, [ 'url', 'runnerToken', 'runnerName', 'runnerDescription' ])
260 })
261
262 return ConfigManager.Instance.setRegisteredInstances(data)
263 }
264
265 private canProcessMoreJobs () {
266 return this.processingJobs.length < ConfigManager.Instance.getConfig().jobs.concurrency
267 }
268
269 // ---------------------------------------------------------------------------
270
271 private async cleanupTMP () {
272 const files = await readdir(ConfigManager.Instance.getTranscodingDirectory())
273
274 for (const file of files) {
275 await remove(join(ConfigManager.Instance.getTranscodingDirectory(), file))
276 }
277 }
278
279 private async onExit () {
280 if (this.cleaningUp) return
281 this.cleaningUp = true
282
283 logger.info('Cleaning up after program exit')
284
285 try {
286 for (const { server, job } of this.processingJobs) {
287 await server.runnerJobs.abort({
288 jobToken: job.jobToken,
289 jobUUID: job.uuid,
290 reason: 'Runner stopped',
291 runnerToken: server.runnerToken
292 })
293 }
294
295 await this.cleanupTMP()
296 } catch (err) {
297 logger.error(err)
298 process.exit(-1)
299 }
300
301 process.exit()
302 }
303
304 static get Instance () {
305 return this.instance || (this.instance = new this())
306 }
307}
diff --git a/apps/peertube-runner/src/server/shared/index.ts b/apps/peertube-runner/src/server/shared/index.ts
new file mode 100644
index 000000000..34d51196b
--- /dev/null
+++ b/apps/peertube-runner/src/server/shared/index.ts
@@ -0,0 +1 @@
export * from './supported-job.js'
diff --git a/apps/peertube-runner/src/server/shared/supported-job.ts b/apps/peertube-runner/src/server/shared/supported-job.ts
new file mode 100644
index 000000000..d905b5de2
--- /dev/null
+++ b/apps/peertube-runner/src/server/shared/supported-job.ts
@@ -0,0 +1,43 @@
1import {
2 RunnerJobLiveRTMPHLSTranscodingPayload,
3 RunnerJobPayload,
4 RunnerJobType,
5 RunnerJobStudioTranscodingPayload,
6 RunnerJobVODAudioMergeTranscodingPayload,
7 RunnerJobVODHLSTranscodingPayload,
8 RunnerJobVODWebVideoTranscodingPayload,
9 VideoStudioTaskPayload
10} from '@peertube/peertube-models'
11
12const supportedMatrix = {
13 'vod-web-video-transcoding': (_payload: RunnerJobVODWebVideoTranscodingPayload) => {
14 return true
15 },
16 'vod-hls-transcoding': (_payload: RunnerJobVODHLSTranscodingPayload) => {
17 return true
18 },
19 'vod-audio-merge-transcoding': (_payload: RunnerJobVODAudioMergeTranscodingPayload) => {
20 return true
21 },
22 'live-rtmp-hls-transcoding': (_payload: RunnerJobLiveRTMPHLSTranscodingPayload) => {
23 return true
24 },
25 'video-studio-transcoding': (payload: RunnerJobStudioTranscodingPayload) => {
26 const tasks = payload?.tasks
27 const supported = new Set<VideoStudioTaskPayload['name']>([ 'add-intro', 'add-outro', 'add-watermark', 'cut' ])
28
29 if (!Array.isArray(tasks)) return false
30
31 return tasks.every(t => t && supported.has(t.name))
32 }
33}
34
35export function isJobSupported (job: {
36 type: RunnerJobType
37 payload: RunnerJobPayload
38}) {
39 const fn = supportedMatrix[job.type]
40 if (!fn) return false
41
42 return fn(job.payload as any)
43}
diff --git a/apps/peertube-runner/src/shared/config-manager.ts b/apps/peertube-runner/src/shared/config-manager.ts
new file mode 100644
index 000000000..84a326a16
--- /dev/null
+++ b/apps/peertube-runner/src/shared/config-manager.ts
@@ -0,0 +1,140 @@
1import { parse, stringify } from '@iarna/toml'
2import envPaths from 'env-paths'
3import { ensureDir, pathExists, remove } from 'fs-extra/esm'
4import { readFile, writeFile } from 'fs/promises'
5import merge from 'lodash-es/merge.js'
6import { dirname, join } from 'path'
7import { logger } from '../shared/index.js'
8
9const paths = envPaths('peertube-runner')
10
11type Config = {
12 jobs: {
13 concurrency: number
14 }
15
16 ffmpeg: {
17 threads: number
18 nice: number
19 }
20
21 registeredInstances: {
22 url: string
23 runnerToken: string
24 runnerName: string
25 runnerDescription?: string
26 }[]
27}
28
29export class ConfigManager {
30 private static instance: ConfigManager
31
32 private config: Config = {
33 jobs: {
34 concurrency: 2
35 },
36 ffmpeg: {
37 threads: 2,
38 nice: 20
39 },
40 registeredInstances: []
41 }
42
43 private id: string
44 private configFilePath: string
45
46 private constructor () {}
47
48 init (id: string) {
49 this.id = id
50 this.configFilePath = join(this.getConfigDir(), 'config.toml')
51 }
52
53 async load () {
54 logger.info(`Using ${this.configFilePath} as configuration file`)
55
56 if (this.isTestInstance()) {
57 logger.info('Removing configuration file as we are using the "test" id')
58 await remove(this.configFilePath)
59 }
60
61 await ensureDir(dirname(this.configFilePath))
62
63 if (!await pathExists(this.configFilePath)) {
64 await this.save()
65 }
66
67 const file = await readFile(this.configFilePath, 'utf-8')
68
69 this.config = merge(this.config, parse(file))
70 }
71
72 save () {
73 return writeFile(this.configFilePath, stringify(this.config))
74 }
75
76 // ---------------------------------------------------------------------------
77
78 async setRegisteredInstances (registeredInstances: {
79 url: string
80 runnerToken: string
81 runnerName: string
82 runnerDescription?: string
83 }[]) {
84 this.config.registeredInstances = registeredInstances
85
86 await this.save()
87 }
88
89 // ---------------------------------------------------------------------------
90
91 getConfig () {
92 return this.deepFreeze(this.config)
93 }
94
95 // ---------------------------------------------------------------------------
96
97 getTranscodingDirectory () {
98 return join(paths.cache, this.id, 'transcoding')
99 }
100
101 getSocketDirectory () {
102 return join(paths.data, this.id)
103 }
104
105 getSocketPath () {
106 return join(this.getSocketDirectory(), 'peertube-runner.sock')
107 }
108
109 getConfigDir () {
110 return join(paths.config, this.id)
111 }
112
113 // ---------------------------------------------------------------------------
114
115 isTestInstance () {
116 return typeof this.id === 'string' && this.id.match(/^test-\d$/)
117 }
118
119 // ---------------------------------------------------------------------------
120
121 // Thanks: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
122 private deepFreeze <T extends object> (object: T) {
123 const propNames = Reflect.ownKeys(object)
124
125 // Freeze properties before freezing self
126 for (const name of propNames) {
127 const value = object[name]
128
129 if ((value && typeof value === 'object') || typeof value === 'function') {
130 this.deepFreeze(value)
131 }
132 }
133
134 return Object.freeze({ ...object })
135 }
136
137 static get Instance () {
138 return this.instance || (this.instance = new this())
139 }
140}
diff --git a/apps/peertube-runner/src/shared/http.ts b/apps/peertube-runner/src/shared/http.ts
new file mode 100644
index 000000000..42886279c
--- /dev/null
+++ b/apps/peertube-runner/src/shared/http.ts
@@ -0,0 +1,67 @@
1import { createWriteStream } from 'fs'
2import { remove } from 'fs-extra/esm'
3import { request as requestHTTP } from 'http'
4import { request as requestHTTPS, RequestOptions } from 'https'
5import { logger } from './logger.js'
6
7export function downloadFile (options: {
8 url: string
9 destination: string
10 runnerToken: string
11 jobToken: string
12}) {
13 const { url, destination, runnerToken, jobToken } = options
14
15 logger.debug(`Downloading file ${url}`)
16
17 return new Promise<void>((res, rej) => {
18 const parsed = new URL(url)
19
20 const body = JSON.stringify({
21 runnerToken,
22 jobToken
23 })
24
25 const getOptions: RequestOptions = {
26 method: 'POST',
27 hostname: parsed.hostname,
28 port: parsed.port,
29 path: parsed.pathname,
30 headers: {
31 'Content-Type': 'application/json',
32 'Content-Length': Buffer.byteLength(body, 'utf-8')
33 }
34 }
35
36 const request = getRequest(url)(getOptions, response => {
37 const code = response.statusCode ?? 0
38
39 if (code >= 400) {
40 return rej(new Error(response.statusMessage))
41 }
42
43 const file = createWriteStream(destination)
44 file.on('finish', () => res())
45
46 response.pipe(file)
47 })
48
49 request.on('error', err => {
50 remove(destination)
51 .catch(err => logger.error(err))
52
53 return rej(err)
54 })
55
56 request.write(body)
57 request.end()
58 })
59}
60
61// ---------------------------------------------------------------------------
62
63function getRequest (url: string) {
64 if (url.startsWith('https://')) return requestHTTPS
65
66 return requestHTTP
67}
diff --git a/apps/peertube-runner/src/shared/index.ts b/apps/peertube-runner/src/shared/index.ts
new file mode 100644
index 000000000..951eef55b
--- /dev/null
+++ b/apps/peertube-runner/src/shared/index.ts
@@ -0,0 +1,3 @@
1export * from './config-manager.js'
2export * from './http.js'
3export * from './logger.js'
diff --git a/apps/peertube-runner/src/shared/ipc/index.ts b/apps/peertube-runner/src/shared/ipc/index.ts
new file mode 100644
index 000000000..337d4de16
--- /dev/null
+++ b/apps/peertube-runner/src/shared/ipc/index.ts
@@ -0,0 +1,2 @@
1export * from './ipc-client.js'
2export * from './ipc-server.js'
diff --git a/apps/peertube-runner/src/shared/ipc/ipc-client.ts b/apps/peertube-runner/src/shared/ipc/ipc-client.ts
new file mode 100644
index 000000000..aa5740dd1
--- /dev/null
+++ b/apps/peertube-runner/src/shared/ipc/ipc-client.ts
@@ -0,0 +1,88 @@
1import CliTable3 from 'cli-table3'
2import { ensureDir } from 'fs-extra/esm'
3import { Client as NetIPC } from 'net-ipc'
4import { ConfigManager } from '../config-manager.js'
5import { IPCReponse, IPCReponseData, IPCRequest } from './shared/index.js'
6
7export class IPCClient {
8 private netIPC: NetIPC
9
10 async run () {
11 await ensureDir(ConfigManager.Instance.getSocketDirectory())
12
13 const socketPath = ConfigManager.Instance.getSocketPath()
14
15 this.netIPC = new NetIPC({ path: socketPath })
16
17 try {
18 await this.netIPC.connect()
19 } catch (err) {
20 if (err.code === 'ECONNREFUSED') {
21 throw new Error(
22 'This runner is not currently running in server mode on this system. ' +
23 'Please run it using the `server` command first (in another terminal for example) and then retry your command.'
24 )
25 }
26
27 throw err
28 }
29 }
30
31 async askRegister (options: {
32 url: string
33 registrationToken: string
34 runnerName: string
35 runnerDescription?: string
36 }) {
37 const req: IPCRequest = {
38 type: 'register',
39 ...options
40 }
41
42 const { success, error } = await this.netIPC.request(req) as IPCReponse
43
44 if (success) console.log('PeerTube instance registered')
45 else console.error('Could not register PeerTube instance on runner server side', error)
46 }
47
48 async askUnregister (options: {
49 url: string
50 runnerName: string
51 }) {
52 const req: IPCRequest = {
53 type: 'unregister',
54 ...options
55 }
56
57 const { success, error } = await this.netIPC.request(req) as IPCReponse
58
59 if (success) console.log('PeerTube instance unregistered')
60 else console.error('Could not unregister PeerTube instance on runner server side', error)
61 }
62
63 async askListRegistered () {
64 const req: IPCRequest = {
65 type: 'list-registered'
66 }
67
68 const { success, error, data } = await this.netIPC.request(req) as IPCReponse<IPCReponseData>
69 if (!success) {
70 console.error('Could not list registered PeerTube instances', error)
71 return
72 }
73
74 const table = new CliTable3({
75 head: [ 'instance', 'runner name', 'runner description' ]
76 })
77
78 for (const server of data.servers) {
79 table.push([ server.url, server.runnerName, server.runnerDescription ])
80 }
81
82 console.log(table.toString())
83 }
84
85 stop () {
86 this.netIPC.destroy()
87 }
88}
diff --git a/apps/peertube-runner/src/shared/ipc/ipc-server.ts b/apps/peertube-runner/src/shared/ipc/ipc-server.ts
new file mode 100644
index 000000000..c68438504
--- /dev/null
+++ b/apps/peertube-runner/src/shared/ipc/ipc-server.ts
@@ -0,0 +1,61 @@
1import { ensureDir } from 'fs-extra/esm'
2import { Server as NetIPC } from 'net-ipc'
3import { pick } from '@peertube/peertube-core-utils'
4import { RunnerServer } from '../../server/index.js'
5import { ConfigManager } from '../config-manager.js'
6import { logger } from '../logger.js'
7import { IPCReponse, IPCReponseData, IPCRequest } from './shared/index.js'
8
9export class IPCServer {
10 private netIPC: NetIPC
11 private runnerServer: RunnerServer
12
13 async run (runnerServer: RunnerServer) {
14 this.runnerServer = runnerServer
15
16 await ensureDir(ConfigManager.Instance.getSocketDirectory())
17
18 const socketPath = ConfigManager.Instance.getSocketPath()
19 this.netIPC = new NetIPC({ path: socketPath })
20 await this.netIPC.start()
21
22 logger.info(`IPC socket created on ${socketPath}`)
23
24 this.netIPC.on('request', async (req: IPCRequest, res) => {
25 try {
26 const data = await this.process(req)
27
28 this.sendReponse(res, { success: true, data })
29 } catch (err) {
30 logger.error('Cannot execute RPC call', err)
31 this.sendReponse(res, { success: false, error: err.message })
32 }
33 })
34 }
35
36 private async process (req: IPCRequest) {
37 switch (req.type) {
38 case 'register':
39 await this.runnerServer.registerRunner(pick(req, [ 'url', 'registrationToken', 'runnerName', 'runnerDescription' ]))
40 return undefined
41
42 case 'unregister':
43 await this.runnerServer.unregisterRunner(pick(req, [ 'url', 'runnerName' ]))
44 return undefined
45
46 case 'list-registered':
47 return Promise.resolve(this.runnerServer.listRegistered())
48
49 default:
50 throw new Error('Unknown RPC call ' + (req as any).type)
51 }
52 }
53
54 private sendReponse <T extends IPCReponseData> (
55 response: (data: any) => Promise<void>,
56 body: IPCReponse<T>
57 ) {
58 response(body)
59 .catch(err => logger.error('Cannot send response after IPC request', err))
60 }
61}
diff --git a/apps/peertube-runner/src/shared/ipc/shared/index.ts b/apps/peertube-runner/src/shared/ipc/shared/index.ts
new file mode 100644
index 000000000..986acafb0
--- /dev/null
+++ b/apps/peertube-runner/src/shared/ipc/shared/index.ts
@@ -0,0 +1,2 @@
1export * from './ipc-request.model.js'
2export * from './ipc-response.model.js'
diff --git a/apps/peertube-runner/src/shared/ipc/shared/ipc-request.model.ts b/apps/peertube-runner/src/shared/ipc/shared/ipc-request.model.ts
new file mode 100644
index 000000000..352808c74
--- /dev/null
+++ b/apps/peertube-runner/src/shared/ipc/shared/ipc-request.model.ts
@@ -0,0 +1,15 @@
1export type IPCRequest =
2 IPCRequestRegister |
3 IPCRequestUnregister |
4 IPCRequestListRegistered
5
6export type IPCRequestRegister = {
7 type: 'register'
8 url: string
9 registrationToken: string
10 runnerName: string
11 runnerDescription?: string
12}
13
14export type IPCRequestUnregister = { type: 'unregister', url: string, runnerName: string }
15export type IPCRequestListRegistered = { type: 'list-registered' }
diff --git a/apps/peertube-runner/src/shared/ipc/shared/ipc-response.model.ts b/apps/peertube-runner/src/shared/ipc/shared/ipc-response.model.ts
new file mode 100644
index 000000000..689d6e09a
--- /dev/null
+++ b/apps/peertube-runner/src/shared/ipc/shared/ipc-response.model.ts
@@ -0,0 +1,15 @@
1export type IPCReponse <T extends IPCReponseData = undefined> = {
2 success: boolean
3 error?: string
4 data?: T
5}
6
7export type IPCReponseData =
8 // list registered
9 {
10 servers: {
11 runnerName: string
12 runnerDescription: string
13 url: string
14 }[]
15 }
diff --git a/apps/peertube-runner/src/shared/logger.ts b/apps/peertube-runner/src/shared/logger.ts
new file mode 100644
index 000000000..ef5283892
--- /dev/null
+++ b/apps/peertube-runner/src/shared/logger.ts
@@ -0,0 +1,12 @@
1import { pino } from 'pino'
2import pretty from 'pino-pretty'
3
4const logger = pino(pretty.default({
5 colorize: true
6}))
7
8logger.level = 'info'
9
10export {
11 logger
12}
diff --git a/apps/peertube-runner/tsconfig.json b/apps/peertube-runner/tsconfig.json
new file mode 100644
index 000000000..03660b0eb
--- /dev/null
+++ b/apps/peertube-runner/tsconfig.json
@@ -0,0 +1,16 @@
1{
2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "baseUrl": "./",
5 "outDir": "./dist",
6 "rootDir": "src",
7 "tsBuildInfoFile": "./dist/.tsbuildinfo"
8 },
9 "references": [
10 { "path": "../../packages/core-utils" },
11 { "path": "../../packages/ffmpeg" },
12 { "path": "../../packages/models" },
13 { "path": "../../packages/node-utils" },
14 { "path": "../../packages/server-commands" }
15 ]
16}
diff --git a/apps/peertube-runner/yarn.lock b/apps/peertube-runner/yarn.lock
new file mode 100644
index 000000000..adb5aa118
--- /dev/null
+++ b/apps/peertube-runner/yarn.lock
@@ -0,0 +1,528 @@
1# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2# yarn lockfile v1
3
4
5"@commander-js/extra-typings@^10.0.3":
6 version "10.0.3"
7 resolved "https://registry.yarnpkg.com/@commander-js/extra-typings/-/extra-typings-10.0.3.tgz#8b6c64897231ed9c00461db82018b5131b653aae"
8 integrity sha512-OIw28QV/GlP8k0B5CJTRsl8IyNvd0R8C8rfo54Yz9P388vCNDgdNrFlKxZTGqps+5j6lSw3Ss9JTQwcur1w1oA==
9
10"@esbuild/android-arm64@0.17.15":
11 version "0.17.15"
12 resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.15.tgz#893ad71f3920ccb919e1757c387756a9bca2ef42"
13 integrity sha512-0kOB6Y7Br3KDVgHeg8PRcvfLkq+AccreK///B4Z6fNZGr/tNHX0z2VywCc7PTeWp+bPvjA5WMvNXltHw5QjAIA==
14
15"@esbuild/android-arm@0.17.15":
16 version "0.17.15"
17 resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.15.tgz#143e0d4e4c08c786ea410b9a7739779a9a1315d8"
18 integrity sha512-sRSOVlLawAktpMvDyJIkdLI/c/kdRTOqo8t6ImVxg8yT7LQDUYV5Rp2FKeEosLr6ZCja9UjYAzyRSxGteSJPYg==
19
20"@esbuild/android-x64@0.17.15":
21 version "0.17.15"
22 resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.15.tgz#d2d12a7676b2589864281b2274355200916540bc"
23 integrity sha512-MzDqnNajQZ63YkaUWVl9uuhcWyEyh69HGpMIrf+acR4otMkfLJ4sUCxqwbCyPGicE9dVlrysI3lMcDBjGiBBcQ==
24
25"@esbuild/darwin-arm64@0.17.15":
26 version "0.17.15"
27 resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.15.tgz#2e88e79f1d327a2a7d9d06397e5232eb0a473d61"
28 integrity sha512-7siLjBc88Z4+6qkMDxPT2juf2e8SJxmsbNVKFY2ifWCDT72v5YJz9arlvBw5oB4W/e61H1+HDB/jnu8nNg0rLA==
29
30"@esbuild/darwin-x64@0.17.15":
31 version "0.17.15"
32 resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.15.tgz#9384e64c0be91388c57be6d3a5eaf1c32a99c91d"
33 integrity sha512-NbImBas2rXwYI52BOKTW342Tm3LTeVlaOQ4QPZ7XuWNKiO226DisFk/RyPk3T0CKZkKMuU69yOvlapJEmax7cg==
34
35"@esbuild/freebsd-arm64@0.17.15":
36 version "0.17.15"
37 resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.15.tgz#2ad5a35bc52ebd9ca6b845dbc59ba39647a93c1a"
38 integrity sha512-Xk9xMDjBVG6CfgoqlVczHAdJnCs0/oeFOspFap5NkYAmRCT2qTn1vJWA2f419iMtsHSLm+O8B6SLV/HlY5cYKg==
39
40"@esbuild/freebsd-x64@0.17.15":
41 version "0.17.15"
42 resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.15.tgz#b513a48446f96c75fda5bef470e64d342d4379cd"
43 integrity sha512-3TWAnnEOdclvb2pnfsTWtdwthPfOz7qAfcwDLcfZyGJwm1SRZIMOeB5FODVhnM93mFSPsHB9b/PmxNNbSnd0RQ==
44
45"@esbuild/linux-arm64@0.17.15":
46 version "0.17.15"
47 resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.15.tgz#9697b168175bfd41fa9cc4a72dd0d48f24715f31"
48 integrity sha512-T0MVnYw9KT6b83/SqyznTs/3Jg2ODWrZfNccg11XjDehIved2oQfrX/wVuev9N936BpMRaTR9I1J0tdGgUgpJA==
49
50"@esbuild/linux-arm@0.17.15":
51 version "0.17.15"
52 resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.15.tgz#5b22062c54f48cd92fab9ffd993732a52db70cd3"
53 integrity sha512-MLTgiXWEMAMr8nmS9Gigx43zPRmEfeBfGCwxFQEMgJ5MC53QKajaclW6XDPjwJvhbebv+RzK05TQjvH3/aM4Xw==
54
55"@esbuild/linux-ia32@0.17.15":
56 version "0.17.15"
57 resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.15.tgz#eb28a13f9b60b5189fcc9e98e1024f6b657ba54c"
58 integrity sha512-wp02sHs015T23zsQtU4Cj57WiteiuASHlD7rXjKUyAGYzlOKDAjqK6bk5dMi2QEl/KVOcsjwL36kD+WW7vJt8Q==
59
60"@esbuild/linux-loong64@0.17.15":
61 version "0.17.15"
62 resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.15.tgz#32454bdfe144cf74b77895a8ad21a15cb81cfbe5"
63 integrity sha512-k7FsUJjGGSxwnBmMh8d7IbObWu+sF/qbwc+xKZkBe/lTAF16RqxRCnNHA7QTd3oS2AfGBAnHlXL67shV5bBThQ==
64
65"@esbuild/linux-mips64el@0.17.15":
66 version "0.17.15"
67 resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.15.tgz#af12bde0d775a318fad90eb13a0455229a63987c"
68 integrity sha512-ZLWk6czDdog+Q9kE/Jfbilu24vEe/iW/Sj2d8EVsmiixQ1rM2RKH2n36qfxK4e8tVcaXkvuV3mU5zTZviE+NVQ==
69
70"@esbuild/linux-ppc64@0.17.15":
71 version "0.17.15"
72 resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.15.tgz#34c5ed145b2dfc493d3e652abac8bd3baa3865a5"
73 integrity sha512-mY6dPkIRAiFHRsGfOYZC8Q9rmr8vOBZBme0/j15zFUKM99d4ILY4WpOC7i/LqoY+RE7KaMaSfvY8CqjJtuO4xg==
74
75"@esbuild/linux-riscv64@0.17.15":
76 version "0.17.15"
77 resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.15.tgz#87bd515e837f2eb004b45f9e6a94dc5b93f22b92"
78 integrity sha512-EcyUtxffdDtWjjwIH8sKzpDRLcVtqANooMNASO59y+xmqqRYBBM7xVLQhqF7nksIbm2yHABptoioS9RAbVMWVA==
79
80"@esbuild/linux-s390x@0.17.15":
81 version "0.17.15"
82 resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.15.tgz#20bf7947197f199ddac2ec412029a414ceae3aa3"
83 integrity sha512-BuS6Jx/ezxFuHxgsfvz7T4g4YlVrmCmg7UAwboeyNNg0OzNzKsIZXpr3Sb/ZREDXWgt48RO4UQRDBxJN3B9Rbg==
84
85"@esbuild/linux-x64@0.17.15":
86 version "0.17.15"
87 resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.15.tgz#31b93f9c94c195e852c20cd3d1914a68aa619124"
88 integrity sha512-JsdS0EgEViwuKsw5tiJQo9UdQdUJYuB+Mf6HxtJSPN35vez1hlrNb1KajvKWF5Sa35j17+rW1ECEO9iNrIXbNg==
89
90"@esbuild/netbsd-x64@0.17.15":
91 version "0.17.15"
92 resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.15.tgz#8da299b3ac6875836ca8cdc1925826498069ac65"
93 integrity sha512-R6fKjtUysYGym6uXf6qyNephVUQAGtf3n2RCsOST/neIwPqRWcnc3ogcielOd6pT+J0RDR1RGcy0ZY7d3uHVLA==
94
95"@esbuild/openbsd-x64@0.17.15":
96 version "0.17.15"
97 resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.15.tgz#04a1ec3d4e919714dba68dcf09eeb1228ad0d20c"
98 integrity sha512-mVD4PGc26b8PI60QaPUltYKeSX0wxuy0AltC+WCTFwvKCq2+OgLP4+fFd+hZXzO2xW1HPKcytZBdjqL6FQFa7w==
99
100"@esbuild/sunos-x64@0.17.15":
101 version "0.17.15"
102 resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.15.tgz#6694ebe4e16e5cd7dab6505ff7c28f9c1c695ce5"
103 integrity sha512-U6tYPovOkw3459t2CBwGcFYfFRjivcJJc1WC8Q3funIwX8x4fP+R6xL/QuTPNGOblbq/EUDxj9GU+dWKX0oWlQ==
104
105"@esbuild/win32-arm64@0.17.15":
106 version "0.17.15"
107 resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.15.tgz#1f95b2564193c8d1fee8f8129a0609728171d500"
108 integrity sha512-W+Z5F++wgKAleDABemiyXVnzXgvRFs+GVKThSI+mGgleLWluv0D7Diz4oQpgdpNzh4i2nNDzQtWbjJiqutRp6Q==
109
110"@esbuild/win32-ia32@0.17.15":
111 version "0.17.15"
112 resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.15.tgz#c362b88b3df21916ed7bcf75c6d09c6bf3ae354a"
113 integrity sha512-Muz/+uGgheShKGqSVS1KsHtCyEzcdOn/W/Xbh6H91Etm+wiIfwZaBn1W58MeGtfI8WA961YMHFYTthBdQs4t+w==
114
115"@esbuild/win32-x64@0.17.15":
116 version "0.17.15"
117 resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.15.tgz#c2e737f3a201ebff8e2ac2b8e9f246b397ad19b8"
118 integrity sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA==
119
120"@iarna/toml@^2.2.5":
121 version "2.2.5"
122 resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c"
123 integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==
124
125"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2":
126 version "3.0.2"
127 resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz#44d752c1a2dc113f15f781b7cc4f53a307e3fa38"
128 integrity sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==
129
130"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.2":
131 version "3.0.2"
132 resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz#f954f34355712212a8e06c465bc06c40852c6bb3"
133 integrity sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==
134
135"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.2":
136 version "3.0.2"
137 resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz#45c63037f045c2b15c44f80f0393fa24f9655367"
138 integrity sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==
139
140"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.2":
141 version "3.0.2"
142 resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz#35707efeafe6d22b3f373caf9e8775e8920d1399"
143 integrity sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==
144
145"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.2":
146 version "3.0.2"
147 resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz#091b1218b66c341f532611477ef89e83f25fae4f"
148 integrity sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==
149
150"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2":
151 version "3.0.2"
152 resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz#0f164b726869f71da3c594171df5ebc1c4b0a407"
153 integrity sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==
154
155abort-controller@^3.0.0:
156 version "3.0.0"
157 resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
158 integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
159 dependencies:
160 event-target-shim "^5.0.0"
161
162atomic-sleep@^1.0.0:
163 version "1.0.0"
164 resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
165 integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
166
167balanced-match@^1.0.0:
168 version "1.0.2"
169 resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
170 integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
171
172base64-js@^1.3.1:
173 version "1.5.1"
174 resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
175 integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
176
177brace-expansion@^2.0.1:
178 version "2.0.1"
179 resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
180 integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
181 dependencies:
182 balanced-match "^1.0.0"
183
184buffer@^6.0.3:
185 version "6.0.3"
186 resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
187 integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
188 dependencies:
189 base64-js "^1.3.1"
190 ieee754 "^1.2.1"
191
192colorette@^2.0.7:
193 version "2.0.19"
194 resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
195 integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
196
197dateformat@^4.6.3:
198 version "4.6.3"
199 resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
200 integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
201
202end-of-stream@^1.1.0:
203 version "1.4.4"
204 resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
205 integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
206 dependencies:
207 once "^1.4.0"
208
209env-paths@^3.0.0:
210 version "3.0.0"
211 resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-3.0.0.tgz#2f1e89c2f6dbd3408e1b1711dd82d62e317f58da"
212 integrity sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==
213
214esbuild@^0.17.15:
215 version "0.17.15"
216 resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.15.tgz#209ebc87cb671ffb79574db93494b10ffaf43cbc"
217 integrity sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw==
218 optionalDependencies:
219 "@esbuild/android-arm" "0.17.15"
220 "@esbuild/android-arm64" "0.17.15"
221 "@esbuild/android-x64" "0.17.15"
222 "@esbuild/darwin-arm64" "0.17.15"
223 "@esbuild/darwin-x64" "0.17.15"
224 "@esbuild/freebsd-arm64" "0.17.15"
225 "@esbuild/freebsd-x64" "0.17.15"
226 "@esbuild/linux-arm" "0.17.15"
227 "@esbuild/linux-arm64" "0.17.15"
228 "@esbuild/linux-ia32" "0.17.15"
229 "@esbuild/linux-loong64" "0.17.15"
230 "@esbuild/linux-mips64el" "0.17.15"
231 "@esbuild/linux-ppc64" "0.17.15"
232 "@esbuild/linux-riscv64" "0.17.15"
233 "@esbuild/linux-s390x" "0.17.15"
234 "@esbuild/linux-x64" "0.17.15"
235 "@esbuild/netbsd-x64" "0.17.15"
236 "@esbuild/openbsd-x64" "0.17.15"
237 "@esbuild/sunos-x64" "0.17.15"
238 "@esbuild/win32-arm64" "0.17.15"
239 "@esbuild/win32-ia32" "0.17.15"
240 "@esbuild/win32-x64" "0.17.15"
241
242event-target-shim@^5.0.0:
243 version "5.0.1"
244 resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
245 integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
246
247events@^3.3.0:
248 version "3.3.0"
249 resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
250 integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
251
252fast-copy@^3.0.0:
253 version "3.0.1"
254 resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.1.tgz#9e89ef498b8c04c1cd76b33b8e14271658a732aa"
255 integrity sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==
256
257fast-redact@^3.1.1:
258 version "3.1.2"
259 resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa"
260 integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==
261
262fast-safe-stringify@^2.1.1:
263 version "2.1.1"
264 resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
265 integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
266
267fast-zlib@^2.0.1:
268 version "2.0.1"
269 resolved "https://registry.yarnpkg.com/fast-zlib/-/fast-zlib-2.0.1.tgz#be624f592fc80ad8019ee2025d16a367a4e9b024"
270 integrity sha512-DCoYgNagM2Bt1VIpXpdGnRx4LzqJeYG0oh6Nf/7cWo6elTXkFGMw9CrRCYYUIapYNrozYMoyDRflx9mgT3Awyw==
271
272fs.realpath@^1.0.0:
273 version "1.0.0"
274 resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
275 integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
276
277glob@^8.0.0:
278 version "8.1.0"
279 resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
280 integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
281 dependencies:
282 fs.realpath "^1.0.0"
283 inflight "^1.0.4"
284 inherits "2"
285 minimatch "^5.0.1"
286 once "^1.3.0"
287
288help-me@^4.0.1:
289 version "4.2.0"
290 resolved "https://registry.yarnpkg.com/help-me/-/help-me-4.2.0.tgz#50712bfd799ff1854ae1d312c36eafcea85b0563"
291 integrity sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==
292 dependencies:
293 glob "^8.0.0"
294 readable-stream "^3.6.0"
295
296ieee754@^1.2.1:
297 version "1.2.1"
298 resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
299 integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
300
301inflight@^1.0.4:
302 version "1.0.6"
303 resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
304 integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
305 dependencies:
306 once "^1.3.0"
307 wrappy "1"
308
309inherits@2, inherits@^2.0.3:
310 version "2.0.4"
311 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
312 integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
313
314joycon@^3.1.1:
315 version "3.1.1"
316 resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
317 integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
318
319minimatch@^5.0.1:
320 version "5.1.6"
321 resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
322 integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
323 dependencies:
324 brace-expansion "^2.0.1"
325
326minimist@^1.2.6:
327 version "1.2.8"
328 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
329 integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
330
331msgpackr-extract@^3.0.1:
332 version "3.0.2"
333 resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz#e05ec1bb4453ddf020551bcd5daaf0092a2c279d"
334 integrity sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==
335 dependencies:
336 node-gyp-build-optional-packages "5.0.7"
337 optionalDependencies:
338 "@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.2"
339 "@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.2"
340 "@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.2"
341 "@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.2"
342 "@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.2"
343 "@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.2"
344
345msgpackr@^1.3.2:
346 version "1.8.5"
347 resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.8.5.tgz#8cadfb935357680648f33699d0e833c9179dbfeb"
348 integrity sha512-mpPs3qqTug6ahbblkThoUY2DQdNXcm4IapwOS3Vm/87vmpzLVelvp9h3It1y9l1VPpiFLV11vfOXnmeEwiIXwg==
349 optionalDependencies:
350 msgpackr-extract "^3.0.1"
351
352net-ipc@^2.0.1:
353 version "2.0.1"
354 resolved "https://registry.yarnpkg.com/net-ipc/-/net-ipc-2.0.1.tgz#1da79ca16f1624f2ed1099a124cb065912c595a5"
355 integrity sha512-4HLjZ/Xorj4kxA7WUajF2EAXlS+OR+XliDLkqQA53Wm7eIr/hWLjdXt4zzB6q4Ii8BB+HbuRbM9yLov3+ttRUw==
356 optionalDependencies:
357 fast-zlib "^2.0.1"
358 msgpackr "^1.3.2"
359
360node-gyp-build-optional-packages@5.0.7:
361 version "5.0.7"
362 resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz#5d2632bbde0ab2f6e22f1bbac2199b07244ae0b3"
363 integrity sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==
364
365on-exit-leak-free@^2.1.0:
366 version "2.1.0"
367 resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4"
368 integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==
369
370once@^1.3.0, once@^1.3.1, once@^1.4.0:
371 version "1.4.0"
372 resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
373 integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
374 dependencies:
375 wrappy "1"
376
377pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.0.0:
378 version "1.0.0"
379 resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3"
380 integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==
381 dependencies:
382 readable-stream "^4.0.0"
383 split2 "^4.0.0"
384
385pino-pretty@^10.0.0:
386 version "10.0.0"
387 resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-10.0.0.tgz#fd2f307ee897289f63d09b0b804ac2ecc9a18516"
388 integrity sha512-zKFjYXBzLaLTEAN1ayKpHXtL5UeRQC7R3lvhKe7fWs7hIVEjKGG/qIXwQt9HmeUp71ogUd/YcW+LmMwRp4KT6Q==
389 dependencies:
390 colorette "^2.0.7"
391 dateformat "^4.6.3"
392 fast-copy "^3.0.0"
393 fast-safe-stringify "^2.1.1"
394 help-me "^4.0.1"
395 joycon "^3.1.1"
396 minimist "^1.2.6"
397 on-exit-leak-free "^2.1.0"
398 pino-abstract-transport "^1.0.0"
399 pump "^3.0.0"
400 readable-stream "^4.0.0"
401 secure-json-parse "^2.4.0"
402 sonic-boom "^3.0.0"
403 strip-json-comments "^3.1.1"
404
405pino-std-serializers@^6.0.0:
406 version "6.2.0"
407 resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.0.tgz#169048c0df3f61352fce56aeb7fb962f1b66ab43"
408 integrity sha512-IWgSzUL8X1w4BIWTwErRgtV8PyOGOOi60uqv0oKuS/fOA8Nco/OeI6lBuc4dyP8MMfdFwyHqTMcBIA7nDiqEqA==
409
410pino@^8.11.0:
411 version "8.11.0"
412 resolved "https://registry.yarnpkg.com/pino/-/pino-8.11.0.tgz#2a91f454106b13e708a66c74ebc1c2ab7ab38498"
413 integrity sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg==
414 dependencies:
415 atomic-sleep "^1.0.0"
416 fast-redact "^3.1.1"
417 on-exit-leak-free "^2.1.0"
418 pino-abstract-transport v1.0.0
419 pino-std-serializers "^6.0.0"
420 process-warning "^2.0.0"
421 quick-format-unescaped "^4.0.3"
422 real-require "^0.2.0"
423 safe-stable-stringify "^2.3.1"
424 sonic-boom "^3.1.0"
425 thread-stream "^2.0.0"
426
427process-warning@^2.0.0:
428 version "2.2.0"
429 resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.2.0.tgz#008ec76b579820a8e5c35d81960525ca64feb626"
430 integrity sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==
431
432process@^0.11.10:
433 version "0.11.10"
434 resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
435 integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
436
437pump@^3.0.0:
438 version "3.0.0"
439 resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
440 integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
441 dependencies:
442 end-of-stream "^1.1.0"
443 once "^1.3.1"
444
445quick-format-unescaped@^4.0.3:
446 version "4.0.4"
447 resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
448 integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
449
450readable-stream@^3.6.0:
451 version "3.6.2"
452 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
453 integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
454 dependencies:
455 inherits "^2.0.3"
456 string_decoder "^1.1.1"
457 util-deprecate "^1.0.1"
458
459readable-stream@^4.0.0:
460 version "4.3.0"
461 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.3.0.tgz#0914d0c72db03b316c9733bb3461d64a3cc50cba"
462 integrity sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==
463 dependencies:
464 abort-controller "^3.0.0"
465 buffer "^6.0.3"
466 events "^3.3.0"
467 process "^0.11.10"
468
469real-require@^0.2.0:
470 version "0.2.0"
471 resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
472 integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
473
474safe-buffer@~5.2.0:
475 version "5.2.1"
476 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
477 integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
478
479safe-stable-stringify@^2.3.1:
480 version "2.4.3"
481 resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886"
482 integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==
483
484secure-json-parse@^2.4.0:
485 version "2.7.0"
486 resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862"
487 integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==
488
489sonic-boom@^3.0.0, sonic-boom@^3.1.0:
490 version "3.3.0"
491 resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.3.0.tgz#cffab6dafee3b2bcb88d08d589394198bee1838c"
492 integrity sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==
493 dependencies:
494 atomic-sleep "^1.0.0"
495
496split2@^4.0.0:
497 version "4.2.0"
498 resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
499 integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
500
501string_decoder@^1.1.1:
502 version "1.3.0"
503 resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
504 integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
505 dependencies:
506 safe-buffer "~5.2.0"
507
508strip-json-comments@^3.1.1:
509 version "3.1.1"
510 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
511 integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
512
513thread-stream@^2.0.0:
514 version "2.3.0"
515 resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.3.0.tgz#4fc07fb39eff32ae7bad803cb7dd9598349fed33"
516 integrity sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==
517 dependencies:
518 real-require "^0.2.0"
519
520util-deprecate@^1.0.1:
521 version "1.0.2"
522 resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
523 integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
524
525wrappy@1:
526 version "1.0.2"
527 resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
528 integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==