diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-31 14:34:36 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-11 15:02:33 +0200 |
commit | 3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch) | |
tree | e4510b39bdac9c318fdb4b47018d08f15368b8f0 /apps | |
parent | 04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff) | |
download | PeerTube-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')
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 @@ | |||
1 | src | ||
2 | meta.json | ||
3 | tsconfig.json | ||
4 | scripts | ||
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 | |||
5 | See https://docs.joinpeertube.org/maintain/tools#remote-tools | ||
6 | |||
7 | ## Dev | ||
8 | |||
9 | ## Install dependencies | ||
10 | |||
11 | ```bash | ||
12 | cd peertube-root | ||
13 | yarn install --pure-lockfile | ||
14 | cd apps/peertube-cli && yarn install --pure-lockfile | ||
15 | ``` | ||
16 | |||
17 | ## Develop | ||
18 | |||
19 | ```bash | ||
20 | cd peertube-root | ||
21 | npm run dev:peertube-cli | ||
22 | ``` | ||
23 | |||
24 | ## Build | ||
25 | |||
26 | ```bash | ||
27 | cd peertube-root | ||
28 | npm run build:peertube-cli | ||
29 | ``` | ||
30 | |||
31 | ## Run | ||
32 | |||
33 | ```bash | ||
34 | cd peertube-root | ||
35 | node apps/peertube-cli/dist/peertube-cli.js --help | ||
36 | ``` | ||
37 | |||
38 | ## Publish on NPM | ||
39 | |||
40 | ```bash | ||
41 | cd 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 @@ | |||
1 | import * as esbuild from 'esbuild' | ||
2 | import { readFileSync } from 'fs' | ||
3 | |||
4 | const packageJSON = JSON.parse(readFileSync(new URL('../package.json', import.meta.url))) | ||
5 | |||
6 | export 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 | |||
27 | await 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 @@ | |||
1 | import * as esbuild from 'esbuild' | ||
2 | import { esbuildOptions } from './build.js' | ||
3 | |||
4 | const context = await esbuild.context(esbuildOptions) | ||
5 | |||
6 | // Enable watch mode | ||
7 | await 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 @@ | |||
1 | import CliTable3 from 'cli-table3' | ||
2 | import prompt from 'prompt' | ||
3 | import { Command } from '@commander-js/extra-typings' | ||
4 | import { assignToken, buildServer, getNetrc, getSettings, writeSettings } from './shared/index.js' | ||
5 | |||
6 | export 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 | |||
128 | async 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 | |||
143 | async 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 | |||
160 | function isURLaPeerTubeInstance (url: string) { | ||
161 | return url.startsWith('http://') || url.startsWith('https://') | ||
162 | } | ||
163 | |||
164 | function 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 @@ | |||
1 | import { Command } from '@commander-js/extra-typings' | ||
2 | import { assignToken, buildServer } from './shared/index.js' | ||
3 | |||
4 | export 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 @@ | |||
1 | import CliTable3 from 'cli-table3' | ||
2 | import { isAbsolute } from 'path' | ||
3 | import { Command } from '@commander-js/extra-typings' | ||
4 | import { PluginType, PluginType_Type } from '@peertube/peertube-models' | ||
5 | import { assignToken, buildServer, CommonProgramOptions, getServerCredentials } from './shared/index.js' | ||
6 | |||
7 | export 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 | |||
88 | async 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 | |||
119 | async 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 | |||
137 | async 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 | |||
155 | async 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 @@ | |||
1 | import bytes from 'bytes' | ||
2 | import CliTable3 from 'cli-table3' | ||
3 | import { URL } from 'url' | ||
4 | import { Command } from '@commander-js/extra-typings' | ||
5 | import { forceNumber, uniqify } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, VideoRedundanciesTarget } from '@peertube/peertube-models' | ||
7 | import { assignToken, buildServer, CommonProgramOptions, getServerCredentials } from './shared/index.js' | ||
8 | |||
9 | export 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 | |||
82 | async 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 | |||
128 | async 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 | |||
154 | async 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 @@ | |||
1 | import { access, constants } from 'fs/promises' | ||
2 | import { isAbsolute } from 'path' | ||
3 | import { inspect } from 'util' | ||
4 | import { Command } from '@commander-js/extra-typings' | ||
5 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { PeerTubeServer } from '@peertube/peertube-server-commands' | ||
7 | import { assignToken, buildServer, getServerCredentials, listOptions } from './shared/index.js' | ||
8 | |||
9 | type 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 | |||
31 | export 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 | |||
87 | async 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 | |||
123 | async 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 | |||
3 | import { Command } from '@commander-js/extra-typings' | ||
4 | import { defineAuthProgram } from './peertube-auth.js' | ||
5 | import { defineGetAccessProgram } from './peertube-get-access-token.js' | ||
6 | import { definePluginsProgram } from './peertube-plugins.js' | ||
7 | import { defineRedundancyProgram } from './peertube-redundancy.js' | ||
8 | import { defineUploadProgram } from './peertube-upload.js' | ||
9 | import { getSettings, version } from './shared/index.js' | ||
10 | |||
11 | const program = new Command() | ||
12 | |||
13 | program | ||
14 | .version(version, '-v, --version') | ||
15 | .usage('[command] [options]') | ||
16 | |||
17 | program.addCommand(defineAuthProgram()) | ||
18 | program.addCommand(defineUploadProgram()) | ||
19 | program.addCommand(defineRedundancyProgram()) | ||
20 | program.addCommand(definePluginsProgram()) | ||
21 | program.addCommand(defineGetAccessProgram()) | ||
22 | |||
23 | // help on no command | ||
24 | if (!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 | |||
50 | getSettings() | ||
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 @@ | |||
1 | import applicationConfig from 'application-config' | ||
2 | import { Netrc } from 'netrc-parser' | ||
3 | import { join } from 'path' | ||
4 | import { createLogger, format, transports } from 'winston' | ||
5 | import { UserRole } from '@peertube/peertube-models' | ||
6 | import { getAppNumber, isTestInstance, root } from '@peertube/peertube-node-utils' | ||
7 | import { PeerTubeServer } from '@peertube/peertube-server-commands' | ||
8 | |||
9 | export type CommonProgramOptions = { | ||
10 | url?: string | ||
11 | username?: string | ||
12 | password?: string | ||
13 | } | ||
14 | |||
15 | let configName = 'PeerTube/CLI' | ||
16 | if (isTestInstance()) configName += `-${getAppNumber()}` | ||
17 | |||
18 | const config = applicationConfig(configName) | ||
19 | |||
20 | const version: string = process.env.PACKAGE_VERSION | ||
21 | |||
22 | async 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 | |||
34 | interface Settings { | ||
35 | remotes: any[] | ||
36 | default: number | ||
37 | } | ||
38 | |||
39 | async 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 | |||
52 | async 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 | |||
62 | function writeSettings (settings: Settings) { | ||
63 | return config.write(settings) | ||
64 | } | ||
65 | |||
66 | function deleteSettings () { | ||
67 | return config.trash() | ||
68 | } | ||
69 | |||
70 | function 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 | |||
123 | function listOptions (val: any) { | ||
124 | return val.split(',') | ||
125 | } | ||
126 | |||
127 | function getServerCredentials (options: CommonProgramOptions) { | ||
128 | return Promise.all([ getSettings(), getNetrc() ]) | ||
129 | .then(([ settings, netrc ]) => { | ||
130 | return getRemoteObjectOrDie(options, settings, netrc) | ||
131 | }) | ||
132 | } | ||
133 | |||
134 | function buildServer (url: string) { | ||
135 | return new PeerTubeServer({ url }) | ||
136 | } | ||
137 | |||
138 | async 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 | |||
147 | function 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 | |||
179 | export { | ||
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 | |||
32 | ansi-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 | |||
37 | ansi-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 | |||
44 | application-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 | |||
49 | application-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 | |||
58 | chalk@^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 | |||
67 | cli-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 | |||
76 | color-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 | |||
83 | color-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 | |||
88 | cross-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 | |||
99 | debug@^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 | |||
106 | detect-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 | |||
111 | emoji-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 | |||
116 | error-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 | |||
123 | escape-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 | |||
128 | execa@^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 | |||
141 | get-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 | |||
146 | graceful-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 | |||
151 | has-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 | |||
156 | imurmurhash@^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 | |||
161 | is-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 | |||
166 | is-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 | |||
171 | is-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 | |||
176 | is-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 | |||
181 | is-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 | |||
186 | isexe@^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 | |||
191 | js-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 | |||
196 | json-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 | |||
201 | lines-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 | |||
206 | load-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 | |||
216 | make-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 | |||
223 | ms@^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 | |||
228 | netrc-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 | |||
236 | nice-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 | |||
241 | npm-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 | |||
248 | p-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 | |||
253 | parse-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 | |||
263 | path-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 | |||
268 | semver@^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 | |||
273 | semver@^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 | |||
278 | shebang-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 | |||
285 | shebang-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 | |||
290 | signal-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 | |||
295 | sort-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 | |||
302 | string-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 | |||
311 | strip-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 | |||
318 | strip-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 | |||
323 | strip-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 | |||
328 | supports-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 | |||
335 | type-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 | |||
340 | typedarray-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 | |||
347 | which@^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 | |||
354 | write-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 | |||
364 | write-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 @@ | |||
1 | node_modules | ||
2 | dist | ||
3 | meta.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 @@ | |||
1 | src | ||
2 | meta.json | ||
3 | tsconfig.json | ||
4 | scripts | ||
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 | |||
3 | Runner program to execute jobs (transcoding...) of remote PeerTube instances. | ||
4 | |||
5 | Commands below has to be run at the root of PeerTube git repository. | ||
6 | |||
7 | ## Dev | ||
8 | |||
9 | ### Install dependencies | ||
10 | |||
11 | ```bash | ||
12 | cd peertube-root | ||
13 | yarn install --pure-lockfile | ||
14 | cd apps/peertube-runner && yarn install --pure-lockfile | ||
15 | ``` | ||
16 | |||
17 | ### Develop | ||
18 | |||
19 | ```bash | ||
20 | cd peertube-root | ||
21 | npm run dev:peertube-runner | ||
22 | ``` | ||
23 | |||
24 | ### Build | ||
25 | |||
26 | ```bash | ||
27 | cd peertube-root | ||
28 | npm run build:peertube-runner | ||
29 | ``` | ||
30 | |||
31 | ### Run | ||
32 | |||
33 | ```bash | ||
34 | cd peertube-root | ||
35 | node apps/peertube-runner/dist/peertube-runner.js --help | ||
36 | ``` | ||
37 | |||
38 | ### Publish on NPM | ||
39 | |||
40 | ```bash | ||
41 | cd 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 @@ | |||
1 | import * as esbuild from 'esbuild' | ||
2 | |||
3 | const packageJSON = JSON.parse(readFileSync(new URL('../package.json', import.meta.url))) | ||
4 | |||
5 | export 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 | |||
26 | await 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 | |||
3 | import { Command, InvalidArgumentError } from '@commander-js/extra-typings' | ||
4 | import { listRegistered, registerRunner, unregisterRunner } from './register/index.js' | ||
5 | import { RunnerServer } from './server/index.js' | ||
6 | import { ConfigManager, logger } from './shared/index.js' | ||
7 | |||
8 | const 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 | |||
26 | program.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 | |||
37 | program.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 | |||
53 | program.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 | |||
67 | program.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 | |||
79 | program.parse() | ||
80 | |||
81 | // --------------------------------------------------------------------------- | ||
82 | // Private | ||
83 | // --------------------------------------------------------------------------- | ||
84 | |||
85 | function 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 @@ | |||
1 | import { IPCClient } from '../shared/ipc/index.js' | ||
2 | |||
3 | export 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 | |||
17 | export 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 | |||
29 | export 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 @@ | |||
1 | export * from './shared/index.js' | ||
2 | export * 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 @@ | |||
1 | import { | ||
2 | RunnerJobLiveRTMPHLSTranscodingPayload, | ||
3 | RunnerJobStudioTranscodingPayload, | ||
4 | RunnerJobVODAudioMergeTranscodingPayload, | ||
5 | RunnerJobVODHLSTranscodingPayload, | ||
6 | RunnerJobVODWebVideoTranscodingPayload | ||
7 | } from '@peertube/peertube-models' | ||
8 | import { logger } from '../../shared/index.js' | ||
9 | import { processAudioMergeTranscoding, processHLSTranscoding, ProcessOptions, processWebVideoTranscoding } from './shared/index.js' | ||
10 | import { ProcessLiveRTMPHLSTranscoding } from './shared/process-live.js' | ||
11 | import { processStudioTranscoding } from './shared/process-studio.js' | ||
12 | |||
13 | export 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 @@ | |||
1 | import { remove } from 'fs-extra/esm' | ||
2 | import { join } from 'path' | ||
3 | import { FFmpegEdition, FFmpegLive, FFmpegVOD, getDefaultAvailableEncoders, getDefaultEncodersToTry } from '@peertube/peertube-ffmpeg' | ||
4 | import { RunnerJob, RunnerJobPayload } from '@peertube/peertube-models' | ||
5 | import { buildUUID } from '@peertube/peertube-node-utils' | ||
6 | import { PeerTubeServer } from '@peertube/peertube-server-commands' | ||
7 | import { ConfigManager, downloadFile, logger } from '../../../shared/index.js' | ||
8 | import { getTranscodingLogger } from './transcoding-logger.js' | ||
9 | |||
10 | export type JobWithToken <T extends RunnerJobPayload = RunnerJobPayload> = RunnerJob<T> & { jobToken: string } | ||
11 | |||
12 | export type ProcessOptions <T extends RunnerJobPayload = RunnerJobPayload> = { | ||
13 | server: PeerTubeServer | ||
14 | job: JobWithToken<T> | ||
15 | runnerToken: string | ||
16 | } | ||
17 | |||
18 | export 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 | |||
38 | export 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 | |||
66 | export 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 | |||
84 | export function buildFFmpegLive () { | ||
85 | return new FFmpegLive(getCommonFFmpegOptions()) | ||
86 | } | ||
87 | |||
88 | export function buildFFmpegEdition () { | ||
89 | return new FFmpegEdition(getCommonFFmpegOptions()) | ||
90 | } | ||
91 | |||
92 | function 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 @@ | |||
1 | export * from './common.js' | ||
2 | export * from './process-vod.js' | ||
3 | export * 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 @@ | |||
1 | import { FSWatcher, watch } from 'chokidar' | ||
2 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
3 | import { ensureDir, remove } from 'fs-extra/esm' | ||
4 | import { basename, join } from 'path' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { ffprobePromise, getVideoStreamBitrate, getVideoStreamDimensionsInfo, hasAudioStream } from '@peertube/peertube-ffmpeg' | ||
7 | import { | ||
8 | LiveRTMPHLSTranscodingSuccess, | ||
9 | LiveRTMPHLSTranscodingUpdatePayload, | ||
10 | PeerTubeProblemDocument, | ||
11 | RunnerJobLiveRTMPHLSTranscodingPayload, | ||
12 | ServerErrorCode | ||
13 | } from '@peertube/peertube-models' | ||
14 | import { buildUUID } from '@peertube/peertube-node-utils' | ||
15 | import { ConfigManager } from '../../../shared/config-manager.js' | ||
16 | import { logger } from '../../../shared/index.js' | ||
17 | import { buildFFmpegLive, ProcessOptions } from './common.js' | ||
18 | |||
19 | export 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 @@ | |||
1 | import { remove } from 'fs-extra/esm' | ||
2 | import { join } from 'path' | ||
3 | import { pick } from '@peertube/peertube-core-utils' | ||
4 | import { | ||
5 | RunnerJobStudioTranscodingPayload, | ||
6 | VideoStudioTask, | ||
7 | VideoStudioTaskCutPayload, | ||
8 | VideoStudioTaskIntroPayload, | ||
9 | VideoStudioTaskOutroPayload, | ||
10 | VideoStudioTaskPayload, | ||
11 | VideoStudioTaskWatermarkPayload, | ||
12 | VideoStudioTranscodingSuccess | ||
13 | } from '@peertube/peertube-models' | ||
14 | import { buildUUID } from '@peertube/peertube-node-utils' | ||
15 | import { ConfigManager } from '../../../shared/config-manager.js' | ||
16 | import { logger } from '../../../shared/index.js' | ||
17 | import { buildFFmpegEdition, downloadInputFile, JobWithToken, ProcessOptions, scheduleTranscodingProgress } from './common.js' | ||
18 | |||
19 | export 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 | |||
85 | type TaskProcessorOptions <T extends VideoStudioTaskPayload = VideoStudioTaskPayload> = { | ||
86 | inputPath: string | ||
87 | outputPath: string | ||
88 | task: T | ||
89 | runnerToken: string | ||
90 | job: JobWithToken | ||
91 | } | ||
92 | |||
93 | const 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 | |||
100 | async 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 | |||
109 | async 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 | |||
130 | function 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 | |||
143 | async 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 @@ | |||
1 | import { remove } from 'fs-extra/esm' | ||
2 | import { join } from 'path' | ||
3 | import { | ||
4 | RunnerJobVODAudioMergeTranscodingPayload, | ||
5 | RunnerJobVODHLSTranscodingPayload, | ||
6 | RunnerJobVODWebVideoTranscodingPayload, | ||
7 | VODAudioMergeTranscodingSuccess, | ||
8 | VODHLSTranscodingSuccess, | ||
9 | VODWebVideoTranscodingSuccess | ||
10 | } from '@peertube/peertube-models' | ||
11 | import { buildUUID } from '@peertube/peertube-node-utils' | ||
12 | import { ConfigManager } from '../../../shared/config-manager.js' | ||
13 | import { logger } from '../../../shared/index.js' | ||
14 | import { buildFFmpegVOD, downloadInputFile, ProcessOptions, scheduleTranscodingProgress } from './common.js' | ||
15 | |||
16 | export 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 | |||
74 | export 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 | |||
136 | export 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 @@ | |||
1 | import { logger } from '../../../shared/index.js' | ||
2 | |||
3 | export 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 @@ | |||
1 | import { ensureDir, remove } from 'fs-extra/esm' | ||
2 | import { readdir } from 'fs/promises' | ||
3 | import { join } from 'path' | ||
4 | import { io, Socket } from 'socket.io-client' | ||
5 | import { pick, shuffle, wait } from '@peertube/peertube-core-utils' | ||
6 | import { PeerTubeProblemDocument, ServerErrorCode } from '@peertube/peertube-models' | ||
7 | import { PeerTubeServer as PeerTubeServerCommand } from '@peertube/peertube-server-commands' | ||
8 | import { ConfigManager } from '../shared/index.js' | ||
9 | import { IPCServer } from '../shared/ipc/index.js' | ||
10 | import { logger } from '../shared/logger.js' | ||
11 | import { JobWithToken, processJob } from './process/index.js' | ||
12 | import { isJobSupported } from './shared/index.js' | ||
13 | |||
14 | type PeerTubeServer = PeerTubeServerCommand & { | ||
15 | runnerToken: string | ||
16 | runnerName: string | ||
17 | runnerDescription?: string | ||
18 | } | ||
19 | |||
20 | export 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 @@ | |||
1 | import { | ||
2 | RunnerJobLiveRTMPHLSTranscodingPayload, | ||
3 | RunnerJobPayload, | ||
4 | RunnerJobType, | ||
5 | RunnerJobStudioTranscodingPayload, | ||
6 | RunnerJobVODAudioMergeTranscodingPayload, | ||
7 | RunnerJobVODHLSTranscodingPayload, | ||
8 | RunnerJobVODWebVideoTranscodingPayload, | ||
9 | VideoStudioTaskPayload | ||
10 | } from '@peertube/peertube-models' | ||
11 | |||
12 | const 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 | |||
35 | export 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 @@ | |||
1 | import { parse, stringify } from '@iarna/toml' | ||
2 | import envPaths from 'env-paths' | ||
3 | import { ensureDir, pathExists, remove } from 'fs-extra/esm' | ||
4 | import { readFile, writeFile } from 'fs/promises' | ||
5 | import merge from 'lodash-es/merge.js' | ||
6 | import { dirname, join } from 'path' | ||
7 | import { logger } from '../shared/index.js' | ||
8 | |||
9 | const paths = envPaths('peertube-runner') | ||
10 | |||
11 | type 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 | |||
29 | export 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 @@ | |||
1 | import { createWriteStream } from 'fs' | ||
2 | import { remove } from 'fs-extra/esm' | ||
3 | import { request as requestHTTP } from 'http' | ||
4 | import { request as requestHTTPS, RequestOptions } from 'https' | ||
5 | import { logger } from './logger.js' | ||
6 | |||
7 | export 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 | |||
63 | function 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 @@ | |||
1 | export * from './config-manager.js' | ||
2 | export * from './http.js' | ||
3 | export * 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 @@ | |||
1 | export * from './ipc-client.js' | ||
2 | export * 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 @@ | |||
1 | import CliTable3 from 'cli-table3' | ||
2 | import { ensureDir } from 'fs-extra/esm' | ||
3 | import { Client as NetIPC } from 'net-ipc' | ||
4 | import { ConfigManager } from '../config-manager.js' | ||
5 | import { IPCReponse, IPCReponseData, IPCRequest } from './shared/index.js' | ||
6 | |||
7 | export 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 @@ | |||
1 | import { ensureDir } from 'fs-extra/esm' | ||
2 | import { Server as NetIPC } from 'net-ipc' | ||
3 | import { pick } from '@peertube/peertube-core-utils' | ||
4 | import { RunnerServer } from '../../server/index.js' | ||
5 | import { ConfigManager } from '../config-manager.js' | ||
6 | import { logger } from '../logger.js' | ||
7 | import { IPCReponse, IPCReponseData, IPCRequest } from './shared/index.js' | ||
8 | |||
9 | export 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 @@ | |||
1 | export * from './ipc-request.model.js' | ||
2 | export * 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 @@ | |||
1 | export type IPCRequest = | ||
2 | IPCRequestRegister | | ||
3 | IPCRequestUnregister | | ||
4 | IPCRequestListRegistered | ||
5 | |||
6 | export type IPCRequestRegister = { | ||
7 | type: 'register' | ||
8 | url: string | ||
9 | registrationToken: string | ||
10 | runnerName: string | ||
11 | runnerDescription?: string | ||
12 | } | ||
13 | |||
14 | export type IPCRequestUnregister = { type: 'unregister', url: string, runnerName: string } | ||
15 | export 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 @@ | |||
1 | export type IPCReponse <T extends IPCReponseData = undefined> = { | ||
2 | success: boolean | ||
3 | error?: string | ||
4 | data?: T | ||
5 | } | ||
6 | |||
7 | export 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 @@ | |||
1 | import { pino } from 'pino' | ||
2 | import pretty from 'pino-pretty' | ||
3 | |||
4 | const logger = pino(pretty.default({ | ||
5 | colorize: true | ||
6 | })) | ||
7 | |||
8 | logger.level = 'info' | ||
9 | |||
10 | export { | ||
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 | |||
155 | abort-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 | |||
162 | atomic-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 | |||
167 | balanced-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 | |||
172 | base64-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 | |||
177 | brace-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 | |||
184 | buffer@^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 | |||
192 | colorette@^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 | |||
197 | dateformat@^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 | |||
202 | end-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 | |||
209 | env-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 | |||
214 | esbuild@^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 | |||
242 | event-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 | |||
247 | events@^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 | |||
252 | fast-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 | |||
257 | fast-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 | |||
262 | fast-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 | |||
267 | fast-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 | |||
272 | fs.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 | |||
277 | glob@^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 | |||
288 | help-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 | |||
296 | ieee754@^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 | |||
301 | inflight@^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 | |||
309 | inherits@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 | |||
314 | joycon@^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 | |||
319 | minimatch@^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 | |||
326 | minimist@^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 | |||
331 | msgpackr-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 | |||
345 | msgpackr@^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 | |||
352 | net-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 | |||
360 | node-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 | |||
365 | on-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 | |||
370 | once@^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 | |||
377 | pino-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 | |||
385 | pino-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 | |||
405 | pino-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 | |||
410 | pino@^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 | |||
427 | process-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 | |||
432 | process@^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 | |||
437 | pump@^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 | |||
445 | quick-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 | |||
450 | readable-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 | |||
459 | readable-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 | |||
469 | real-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 | |||
474 | safe-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 | |||
479 | safe-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 | |||
484 | secure-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 | |||
489 | sonic-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 | |||
496 | split2@^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 | |||
501 | string_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 | |||
508 | strip-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 | |||
513 | thread-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 | |||
520 | util-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 | |||
525 | wrappy@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== | ||