diff options
-rw-r--r-- | server/tools/cli.ts | 33 | ||||
-rw-r--r-- | server/tools/peertube-import-videos.ts | 85 |
2 files changed, 81 insertions, 37 deletions
diff --git a/server/tools/cli.ts b/server/tools/cli.ts index 8599a270f..58e2445ac 100644 --- a/server/tools/cli.ts +++ b/server/tools/cli.ts | |||
@@ -5,6 +5,7 @@ import { root } from '../../shared/extra-utils/miscs/miscs' | |||
5 | import { getVideoChannel } from '../../shared/extra-utils/videos/video-channels' | 5 | import { getVideoChannel } from '../../shared/extra-utils/videos/video-channels' |
6 | import { Command } from 'commander' | 6 | import { Command } from 'commander' |
7 | import { VideoChannel, VideoPrivacy } from '../../shared/models/videos' | 7 | import { VideoChannel, VideoPrivacy } from '../../shared/models/videos' |
8 | import { createLogger, format, transports } from 'winston' | ||
8 | 9 | ||
9 | let configName = 'PeerTube/CLI' | 10 | let configName = 'PeerTube/CLI' |
10 | if (isTestInstance()) configName += `-${getAppNumber()}` | 11 | if (isTestInstance()) configName += `-${getAppNumber()}` |
@@ -119,6 +120,7 @@ function buildCommonVideoOptions (command: Command) { | |||
119 | .option('-m, --comments-enabled', 'Enable comments') | 120 | .option('-m, --comments-enabled', 'Enable comments') |
120 | .option('-s, --support <support>', 'Video support text') | 121 | .option('-s, --support <support>', 'Video support text') |
121 | .option('-w, --wait-transcoding', 'Wait transcoding before publishing the video') | 122 | .option('-w, --wait-transcoding', 'Wait transcoding before publishing the video') |
123 | .option('-v, --verbose <verbose>', 'Verbosity, from 0/\'error\' to 4/\'debug\'', 'info') | ||
122 | } | 124 | } |
123 | 125 | ||
124 | async function buildVideoAttributesFromCommander (url: string, command: Command, defaultAttributes: any = {}) { | 126 | async function buildVideoAttributesFromCommander (url: string, command: Command, defaultAttributes: any = {}) { |
@@ -175,11 +177,42 @@ function getServerCredentials (program: any) { | |||
175 | }) | 177 | }) |
176 | } | 178 | } |
177 | 179 | ||
180 | function getLogger (logLevel = 'info') { | ||
181 | const logLevels = { | ||
182 | 0: 0, | ||
183 | error: 0, | ||
184 | 1: 1, | ||
185 | warn: 1, | ||
186 | 2: 2, | ||
187 | info: 2, | ||
188 | 3: 3, | ||
189 | verbose: 3, | ||
190 | 4: 4, | ||
191 | debug: 4 | ||
192 | } | ||
193 | |||
194 | const logger = createLogger({ | ||
195 | levels: logLevels, | ||
196 | format: format.combine( | ||
197 | format.splat(), | ||
198 | format.simple() | ||
199 | ), | ||
200 | transports: [ | ||
201 | new (transports.Console)({ | ||
202 | level: logLevel | ||
203 | }) | ||
204 | ] | ||
205 | }) | ||
206 | |||
207 | return logger | ||
208 | } | ||
209 | |||
178 | // --------------------------------------------------------------------------- | 210 | // --------------------------------------------------------------------------- |
179 | 211 | ||
180 | export { | 212 | export { |
181 | version, | 213 | version, |
182 | config, | 214 | config, |
215 | getLogger, | ||
183 | getSettings, | 216 | getSettings, |
184 | getNetrc, | 217 | getNetrc, |
185 | getRemoteObjectOrDie, | 218 | getRemoteObjectOrDie, |
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts index 0ebfa7442..fcb90cca3 100644 --- a/server/tools/peertube-import-videos.ts +++ b/server/tools/peertube-import-videos.ts | |||
@@ -8,10 +8,11 @@ import { CONSTRAINTS_FIELDS } from '../initializers/constants' | |||
8 | import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../../shared/extra-utils/index' | 8 | import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../../shared/extra-utils/index' |
9 | import { truncate } from 'lodash' | 9 | import { truncate } from 'lodash' |
10 | import * as prompt from 'prompt' | 10 | import * as prompt from 'prompt' |
11 | import { accessSync, constants } from 'fs' | ||
11 | import { remove } from 'fs-extra' | 12 | import { remove } from 'fs-extra' |
12 | import { sha256 } from '../helpers/core-utils' | 13 | import { sha256 } from '../helpers/core-utils' |
13 | import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl' | 14 | import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl' |
14 | import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getServerCredentials } from './cli' | 15 | import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getServerCredentials, getLogger } from './cli' |
15 | 16 | ||
16 | type UserInfo = { | 17 | type UserInfo = { |
17 | username: string | 18 | username: string |
@@ -19,7 +20,6 @@ type UserInfo = { | |||
19 | } | 20 | } |
20 | 21 | ||
21 | const processOptions = { | 22 | const processOptions = { |
22 | cwd: __dirname, | ||
23 | maxBuffer: Infinity | 23 | maxBuffer: Infinity |
24 | } | 24 | } |
25 | 25 | ||
@@ -35,15 +35,23 @@ command | |||
35 | .option('--target-url <targetUrl>', 'Video target URL') | 35 | .option('--target-url <targetUrl>', 'Video target URL') |
36 | .option('--since <since>', 'Publication date (inclusive) since which the videos can be imported (YYYY-MM-DD)', parseDate) | 36 | .option('--since <since>', 'Publication date (inclusive) since which the videos can be imported (YYYY-MM-DD)', parseDate) |
37 | .option('--until <until>', 'Publication date (inclusive) until which the videos can be imported (YYYY-MM-DD)', parseDate) | 37 | .option('--until <until>', 'Publication date (inclusive) until which the videos can be imported (YYYY-MM-DD)', parseDate) |
38 | .option('-v, --verbose', 'Verbose mode') | 38 | .option('--first <first>', 'Process first n elements of returned playlist') |
39 | .option('--last <last>', 'Process last n elements of returned playlist') | ||
40 | .option('-T, --tmpdir <tmpdir>', 'Working directory', __dirname) | ||
39 | .parse(process.argv) | 41 | .parse(process.argv) |
40 | 42 | ||
43 | let log = getLogger(program[ 'verbose' ]) | ||
44 | |||
41 | getServerCredentials(command) | 45 | getServerCredentials(command) |
42 | .then(({ url, username, password }) => { | 46 | .then(({ url, username, password }) => { |
43 | if (!program[ 'targetUrl' ]) { | 47 | if (!program[ 'targetUrl' ]) { |
44 | console.error('--targetUrl field is required.') | 48 | exitError('--target-url field is required.') |
49 | } | ||
45 | 50 | ||
46 | process.exit(-1) | 51 | try { |
52 | accessSync(program[ 'tmpdir' ], constants.R_OK | constants.W_OK) | ||
53 | } catch (e) { | ||
54 | exitError('--tmpdir %s: directory does not exist or is not accessible', program[ 'tmpdir' ]) | ||
47 | } | 55 | } |
48 | 56 | ||
49 | removeEndSlashes(url) | 57 | removeEndSlashes(url) |
@@ -53,8 +61,7 @@ getServerCredentials(command) | |||
53 | 61 | ||
54 | run(url, user) | 62 | run(url, user) |
55 | .catch(err => { | 63 | .catch(err => { |
56 | console.error(err) | 64 | exitError(err) |
57 | process.exit(-1) | ||
58 | }) | 65 | }) |
59 | }) | 66 | }) |
60 | 67 | ||
@@ -68,30 +75,32 @@ async function run (url: string, user: UserInfo) { | |||
68 | const options = [ '-j', '--flat-playlist', '--playlist-reverse' ] | 75 | const options = [ '-j', '--flat-playlist', '--playlist-reverse' ] |
69 | youtubeDL.getInfo(program[ 'targetUrl' ], options, processOptions, async (err, info) => { | 76 | youtubeDL.getInfo(program[ 'targetUrl' ], options, processOptions, async (err, info) => { |
70 | if (err) { | 77 | if (err) { |
71 | console.log(err.message) | 78 | exitError(err.message) |
72 | process.exit(1) | ||
73 | } | 79 | } |
74 | 80 | ||
75 | let infoArray: any[] | 81 | let infoArray: any[] |
76 | 82 | ||
77 | // Normalize utf8 fields | 83 | // Normalize utf8 fields |
78 | if (Array.isArray(info) === true) { | 84 | infoArray = [].concat(info); |
79 | infoArray = info.map(i => normalizeObject(i)) | 85 | if (program[ 'first' ]) { |
80 | } else { | 86 | infoArray = infoArray.slice(0, program[ 'first' ]) |
81 | infoArray = [ normalizeObject(info) ] | 87 | } else if (program[ 'last' ]) { |
88 | infoArray = infoArray.slice(- program[ 'last' ]) | ||
82 | } | 89 | } |
83 | console.log('Will download and upload %d videos.\n', infoArray.length) | 90 | infoArray = infoArray.map(i => normalizeObject(i)) |
91 | |||
92 | log.info('Will download and upload %d videos.\n', infoArray.length) | ||
84 | 93 | ||
85 | for (const info of infoArray) { | 94 | for (const info of infoArray) { |
86 | await processVideo({ | 95 | await processVideo({ |
87 | cwd: processOptions.cwd, | 96 | cwd: program[ 'tmpdir' ], |
88 | url, | 97 | url, |
89 | user, | 98 | user, |
90 | youtubeInfo: info | 99 | youtubeInfo: info |
91 | }) | 100 | }) |
92 | } | 101 | } |
93 | 102 | ||
94 | console.log('Video/s for user %s imported: %s', program[ 'username' ], program[ 'targetUrl' ]) | 103 | log.info('Video/s for user %s imported: %s', user.username, program[ 'targetUrl' ]) |
95 | process.exit(0) | 104 | process.exit(0) |
96 | }) | 105 | }) |
97 | } | 106 | } |
@@ -105,21 +114,21 @@ function processVideo (parameters: { | |||
105 | const { youtubeInfo, cwd, url, user } = parameters | 114 | const { youtubeInfo, cwd, url, user } = parameters |
106 | 115 | ||
107 | return new Promise(async res => { | 116 | return new Promise(async res => { |
108 | if (program[ 'verbose' ]) console.log('Fetching object.', youtubeInfo) | 117 | log.debug('Fetching object.', youtubeInfo) |
109 | 118 | ||
110 | const videoInfo = await fetchObject(youtubeInfo) | 119 | const videoInfo = await fetchObject(youtubeInfo) |
111 | if (program[ 'verbose' ]) console.log('Fetched object.', videoInfo) | 120 | log.debug('Fetched object.', videoInfo) |
112 | 121 | ||
113 | if (program[ 'since' ]) { | 122 | if (program[ 'since' ]) { |
114 | if (buildOriginallyPublishedAt(videoInfo).getTime() < program[ 'since' ].getTime()) { | 123 | if (buildOriginallyPublishedAt(videoInfo).getTime() < program[ 'since' ].getTime()) { |
115 | console.log('Video "%s" has been published before "%s", don\'t upload it.\n', | 124 | log.info('Video "%s" has been published before "%s", don\'t upload it.\n', |
116 | videoInfo.title, formatDate(program[ 'since' ])); | 125 | videoInfo.title, formatDate(program[ 'since' ])); |
117 | return res(); | 126 | return res(); |
118 | } | 127 | } |
119 | } | 128 | } |
120 | if (program[ 'until' ]) { | 129 | if (program[ 'until' ]) { |
121 | if (buildOriginallyPublishedAt(videoInfo).getTime() > program[ 'until' ].getTime()) { | 130 | if (buildOriginallyPublishedAt(videoInfo).getTime() > program[ 'until' ].getTime()) { |
122 | console.log('Video "%s" has been published after "%s", don\'t upload it.\n', | 131 | log.info('Video "%s" has been published after "%s", don\'t upload it.\n', |
123 | videoInfo.title, formatDate(program[ 'until' ])); | 132 | videoInfo.title, formatDate(program[ 'until' ])); |
124 | return res(); | 133 | return res(); |
125 | } | 134 | } |
@@ -127,27 +136,27 @@ function processVideo (parameters: { | |||
127 | 136 | ||
128 | const result = await searchVideoWithSort(url, videoInfo.title, '-match') | 137 | const result = await searchVideoWithSort(url, videoInfo.title, '-match') |
129 | 138 | ||
130 | console.log('############################################################\n') | 139 | log.info('############################################################\n') |
131 | 140 | ||
132 | if (result.body.data.find(v => v.name === videoInfo.title)) { | 141 | if (result.body.data.find(v => v.name === videoInfo.title)) { |
133 | console.log('Video "%s" already exists, don\'t reupload it.\n', videoInfo.title) | 142 | log.info('Video "%s" already exists, don\'t reupload it.\n', videoInfo.title) |
134 | return res() | 143 | return res() |
135 | } | 144 | } |
136 | 145 | ||
137 | const path = join(cwd, sha256(videoInfo.url) + '.mp4') | 146 | const path = join(cwd, sha256(videoInfo.url) + '.mp4') |
138 | 147 | ||
139 | console.log('Downloading video "%s"...', videoInfo.title) | 148 | log.info('Downloading video "%s"...', videoInfo.title) |
140 | 149 | ||
141 | const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ] | 150 | const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ] |
142 | try { | 151 | try { |
143 | const youtubeDL = await safeGetYoutubeDL() | 152 | const youtubeDL = await safeGetYoutubeDL() |
144 | youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => { | 153 | youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => { |
145 | if (err) { | 154 | if (err) { |
146 | console.error(err) | 155 | log.error(err) |
147 | return res() | 156 | return res() |
148 | } | 157 | } |
149 | 158 | ||
150 | console.log(output.join('\n')) | 159 | log.info(output.join('\n')) |
151 | await uploadVideoOnPeerTube({ | 160 | await uploadVideoOnPeerTube({ |
152 | cwd, | 161 | cwd, |
153 | url, | 162 | url, |
@@ -158,7 +167,7 @@ function processVideo (parameters: { | |||
158 | return res() | 167 | return res() |
159 | }) | 168 | }) |
160 | } catch (err) { | 169 | } catch (err) { |
161 | console.log(err.message) | 170 | log.error(err.message) |
162 | return res() | 171 | return res() |
163 | } | 172 | } |
164 | }) | 173 | }) |
@@ -217,7 +226,7 @@ async function uploadVideoOnPeerTube (parameters: { | |||
217 | fixture: videoPath | 226 | fixture: videoPath |
218 | }) | 227 | }) |
219 | 228 | ||
220 | console.log('\nUploading on PeerTube video "%s".', videoAttributes.name) | 229 | log.info('\nUploading on PeerTube video "%s".', videoAttributes.name) |
221 | 230 | ||
222 | let accessToken = await getAccessTokenOrDie(url, user) | 231 | let accessToken = await getAccessTokenOrDie(url, user) |
223 | 232 | ||
@@ -225,21 +234,20 @@ async function uploadVideoOnPeerTube (parameters: { | |||
225 | await uploadVideo(url, accessToken, videoAttributes) | 234 | await uploadVideo(url, accessToken, videoAttributes) |
226 | } catch (err) { | 235 | } catch (err) { |
227 | if (err.message.indexOf('401') !== -1) { | 236 | if (err.message.indexOf('401') !== -1) { |
228 | console.log('Got 401 Unauthorized, token may have expired, renewing token and retry.') | 237 | log.info('Got 401 Unauthorized, token may have expired, renewing token and retry.') |
229 | 238 | ||
230 | accessToken = await getAccessTokenOrDie(url, user) | 239 | accessToken = await getAccessTokenOrDie(url, user) |
231 | 240 | ||
232 | await uploadVideo(url, accessToken, videoAttributes) | 241 | await uploadVideo(url, accessToken, videoAttributes) |
233 | } else { | 242 | } else { |
234 | console.log(err.message) | 243 | exitError(err.message) |
235 | process.exit(1) | ||
236 | } | 244 | } |
237 | } | 245 | } |
238 | 246 | ||
239 | await remove(videoPath) | 247 | await remove(videoPath) |
240 | if (thumbnailfile) await remove(thumbnailfile) | 248 | if (thumbnailfile) await remove(thumbnailfile) |
241 | 249 | ||
242 | console.log('Uploaded video "%s"!\n', videoAttributes.name) | 250 | log.warn('Uploaded video "%s"!\n', videoAttributes.name) |
243 | } | 251 | } |
244 | 252 | ||
245 | /* ---------------------------------------------------------- */ | 253 | /* ---------------------------------------------------------- */ |
@@ -355,20 +363,17 @@ async function getAccessTokenOrDie (url: string, user: UserInfo) { | |||
355 | const res = await login(url, client, user) | 363 | const res = await login(url, client, user) |
356 | return res.body.access_token | 364 | return res.body.access_token |
357 | } catch (err) { | 365 | } catch (err) { |
358 | console.error('Cannot authenticate. Please check your username/password.') | 366 | exitError('Cannot authenticate. Please check your username/password.') |
359 | process.exit(-1) | ||
360 | } | 367 | } |
361 | } | 368 | } |
362 | 369 | ||
363 | function parseDate (dateAsStr: string): Date { | 370 | function parseDate (dateAsStr: string): Date { |
364 | if (!/\d{4}-\d{2}-\d{2}/.test(dateAsStr)) { | 371 | if (!/\d{4}-\d{2}-\d{2}/.test(dateAsStr)) { |
365 | console.error(`Invalid date passed: ${dateAsStr}. Expected format: YYYY-MM-DD. See help for usage.`); | 372 | exitError(`Invalid date passed: ${dateAsStr}. Expected format: YYYY-MM-DD. See help for usage.`); |
366 | process.exit(-1); | ||
367 | } | 373 | } |
368 | const date = new Date(dateAsStr); | 374 | const date = new Date(dateAsStr); |
369 | if (isNaN(date.getTime())) { | 375 | if (isNaN(date.getTime())) { |
370 | console.error(`Invalid date passed: ${dateAsStr}. See help for usage.`); | 376 | exitError(`Invalid date passed: ${dateAsStr}. See help for usage.`); |
371 | process.exit(-1); | ||
372 | } | 377 | } |
373 | return date; | 378 | return date; |
374 | } | 379 | } |
@@ -376,3 +381,9 @@ function parseDate (dateAsStr: string): Date { | |||
376 | function formatDate (date: Date): string { | 381 | function formatDate (date: Date): string { |
377 | return date.toISOString().split('T')[0]; | 382 | return date.toISOString().split('T')[0]; |
378 | } | 383 | } |
384 | |||
385 | function exitError (message:string, ...meta: any[]) { | ||
386 | // use console.error instead of log.error here | ||
387 | console.error(message, ...meta) | ||
388 | process.exit(-1) | ||
389 | } | ||