peertube-import-videos.ts: add --tmpdir, --first, --last and --verbose [level] parameters (#2045)
* peertube-import-videos.ts: add --tmpdir <tmpdir> parameter, used to designate working directory for downloading and converting imported videos * peertube-import-videos.ts: add --first and --last parameters to limit processing of the returned playlist to the first/last N elements * peertube-import-videos.ts: add --verbose [verbosity] parameter, set this from 0 (only errors are reported) to 4 (for trace debugging), default is 2 (info). When --verbose is used without the optional parameter the logging level is set to 3 (debug). At level 1 (warn) it will only report on successfully uploaded videos (and/or errors), use this when running peertube-import-videos in a cron job to mirror a channel. * package.json: remove dependency on loglevel cli.ts: add getLogger(loglevel), to be used in CLI tools, add --verbose to set log level peertube-import-videos: use getLogger (from cli) instead of loglevel, add error_exit (log error and exit), move --verbose to cli.ts, etc. * cli.ts: remove superfluous reference to default logging level * peertube-import-videos: exit_error -> exitError
This commit is contained in:
parent
f3ea7ecee1
commit
bda3b70537
|
@ -5,6 +5,7 @@ import { root } from '../../shared/extra-utils/miscs/miscs'
|
||||||
import { getVideoChannel } from '../../shared/extra-utils/videos/video-channels'
|
import { getVideoChannel } from '../../shared/extra-utils/videos/video-channels'
|
||||||
import { Command } from 'commander'
|
import { Command } from 'commander'
|
||||||
import { VideoChannel, VideoPrivacy } from '../../shared/models/videos'
|
import { VideoChannel, VideoPrivacy } from '../../shared/models/videos'
|
||||||
|
import { createLogger, format, transports } from 'winston'
|
||||||
|
|
||||||
let configName = 'PeerTube/CLI'
|
let configName = 'PeerTube/CLI'
|
||||||
if (isTestInstance()) configName += `-${getAppNumber()}`
|
if (isTestInstance()) configName += `-${getAppNumber()}`
|
||||||
|
@ -119,6 +120,7 @@ function buildCommonVideoOptions (command: Command) {
|
||||||
.option('-m, --comments-enabled', 'Enable comments')
|
.option('-m, --comments-enabled', 'Enable comments')
|
||||||
.option('-s, --support <support>', 'Video support text')
|
.option('-s, --support <support>', 'Video support text')
|
||||||
.option('-w, --wait-transcoding', 'Wait transcoding before publishing the video')
|
.option('-w, --wait-transcoding', 'Wait transcoding before publishing the video')
|
||||||
|
.option('-v, --verbose <verbose>', 'Verbosity, from 0/\'error\' to 4/\'debug\'', 'info')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildVideoAttributesFromCommander (url: string, command: Command, defaultAttributes: any = {}) {
|
async function buildVideoAttributesFromCommander (url: string, command: Command, defaultAttributes: any = {}) {
|
||||||
|
@ -175,11 +177,42 @@ function getServerCredentials (program: any) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLogger (logLevel = 'info') {
|
||||||
|
const logLevels = {
|
||||||
|
0: 0,
|
||||||
|
error: 0,
|
||||||
|
1: 1,
|
||||||
|
warn: 1,
|
||||||
|
2: 2,
|
||||||
|
info: 2,
|
||||||
|
3: 3,
|
||||||
|
verbose: 3,
|
||||||
|
4: 4,
|
||||||
|
debug: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = createLogger({
|
||||||
|
levels: logLevels,
|
||||||
|
format: format.combine(
|
||||||
|
format.splat(),
|
||||||
|
format.simple()
|
||||||
|
),
|
||||||
|
transports: [
|
||||||
|
new (transports.Console)({
|
||||||
|
level: logLevel
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
version,
|
version,
|
||||||
config,
|
config,
|
||||||
|
getLogger,
|
||||||
getSettings,
|
getSettings,
|
||||||
getNetrc,
|
getNetrc,
|
||||||
getRemoteObjectOrDie,
|
getRemoteObjectOrDie,
|
||||||
|
|
|
@ -8,10 +8,11 @@ import { CONSTRAINTS_FIELDS } from '../initializers/constants'
|
||||||
import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../../shared/extra-utils/index'
|
import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../../shared/extra-utils/index'
|
||||||
import { truncate } from 'lodash'
|
import { truncate } from 'lodash'
|
||||||
import * as prompt from 'prompt'
|
import * as prompt from 'prompt'
|
||||||
|
import { accessSync, constants } from 'fs'
|
||||||
import { remove } from 'fs-extra'
|
import { remove } from 'fs-extra'
|
||||||
import { sha256 } from '../helpers/core-utils'
|
import { sha256 } from '../helpers/core-utils'
|
||||||
import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl'
|
import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl'
|
||||||
import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getServerCredentials } from './cli'
|
import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getServerCredentials, getLogger } from './cli'
|
||||||
|
|
||||||
type UserInfo = {
|
type UserInfo = {
|
||||||
username: string
|
username: string
|
||||||
|
@ -19,7 +20,6 @@ type UserInfo = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const processOptions = {
|
const processOptions = {
|
||||||
cwd: __dirname,
|
|
||||||
maxBuffer: Infinity
|
maxBuffer: Infinity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,15 +35,23 @@ command
|
||||||
.option('--target-url <targetUrl>', 'Video target URL')
|
.option('--target-url <targetUrl>', 'Video target URL')
|
||||||
.option('--since <since>', 'Publication date (inclusive) since which the videos can be imported (YYYY-MM-DD)', parseDate)
|
.option('--since <since>', 'Publication date (inclusive) since which the videos can be imported (YYYY-MM-DD)', parseDate)
|
||||||
.option('--until <until>', 'Publication date (inclusive) until which the videos can be imported (YYYY-MM-DD)', parseDate)
|
.option('--until <until>', 'Publication date (inclusive) until which the videos can be imported (YYYY-MM-DD)', parseDate)
|
||||||
.option('-v, --verbose', 'Verbose mode')
|
.option('--first <first>', 'Process first n elements of returned playlist')
|
||||||
|
.option('--last <last>', 'Process last n elements of returned playlist')
|
||||||
|
.option('-T, --tmpdir <tmpdir>', 'Working directory', __dirname)
|
||||||
.parse(process.argv)
|
.parse(process.argv)
|
||||||
|
|
||||||
|
let log = getLogger(program[ 'verbose' ])
|
||||||
|
|
||||||
getServerCredentials(command)
|
getServerCredentials(command)
|
||||||
.then(({ url, username, password }) => {
|
.then(({ url, username, password }) => {
|
||||||
if (!program[ 'targetUrl' ]) {
|
if (!program[ 'targetUrl' ]) {
|
||||||
console.error('--targetUrl field is required.')
|
exitError('--target-url field is required.')
|
||||||
|
}
|
||||||
|
|
||||||
process.exit(-1)
|
try {
|
||||||
|
accessSync(program[ 'tmpdir' ], constants.R_OK | constants.W_OK)
|
||||||
|
} catch (e) {
|
||||||
|
exitError('--tmpdir %s: directory does not exist or is not accessible', program[ 'tmpdir' ])
|
||||||
}
|
}
|
||||||
|
|
||||||
removeEndSlashes(url)
|
removeEndSlashes(url)
|
||||||
|
@ -53,8 +61,7 @@ getServerCredentials(command)
|
||||||
|
|
||||||
run(url, user)
|
run(url, user)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error(err)
|
exitError(err)
|
||||||
process.exit(-1)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -68,30 +75,32 @@ async function run (url: string, user: UserInfo) {
|
||||||
const options = [ '-j', '--flat-playlist', '--playlist-reverse' ]
|
const options = [ '-j', '--flat-playlist', '--playlist-reverse' ]
|
||||||
youtubeDL.getInfo(program[ 'targetUrl' ], options, processOptions, async (err, info) => {
|
youtubeDL.getInfo(program[ 'targetUrl' ], options, processOptions, async (err, info) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(err.message)
|
exitError(err.message)
|
||||||
process.exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let infoArray: any[]
|
let infoArray: any[]
|
||||||
|
|
||||||
// Normalize utf8 fields
|
// Normalize utf8 fields
|
||||||
if (Array.isArray(info) === true) {
|
infoArray = [].concat(info);
|
||||||
infoArray = info.map(i => normalizeObject(i))
|
if (program[ 'first' ]) {
|
||||||
} else {
|
infoArray = infoArray.slice(0, program[ 'first' ])
|
||||||
infoArray = [ normalizeObject(info) ]
|
} else if (program[ 'last' ]) {
|
||||||
|
infoArray = infoArray.slice(- program[ 'last' ])
|
||||||
}
|
}
|
||||||
console.log('Will download and upload %d videos.\n', infoArray.length)
|
infoArray = infoArray.map(i => normalizeObject(i))
|
||||||
|
|
||||||
|
log.info('Will download and upload %d videos.\n', infoArray.length)
|
||||||
|
|
||||||
for (const info of infoArray) {
|
for (const info of infoArray) {
|
||||||
await processVideo({
|
await processVideo({
|
||||||
cwd: processOptions.cwd,
|
cwd: program[ 'tmpdir' ],
|
||||||
url,
|
url,
|
||||||
user,
|
user,
|
||||||
youtubeInfo: info
|
youtubeInfo: info
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Video/s for user %s imported: %s', program[ 'username' ], program[ 'targetUrl' ])
|
log.info('Video/s for user %s imported: %s', user.username, program[ 'targetUrl' ])
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -105,21 +114,21 @@ function processVideo (parameters: {
|
||||||
const { youtubeInfo, cwd, url, user } = parameters
|
const { youtubeInfo, cwd, url, user } = parameters
|
||||||
|
|
||||||
return new Promise(async res => {
|
return new Promise(async res => {
|
||||||
if (program[ 'verbose' ]) console.log('Fetching object.', youtubeInfo)
|
log.debug('Fetching object.', youtubeInfo)
|
||||||
|
|
||||||
const videoInfo = await fetchObject(youtubeInfo)
|
const videoInfo = await fetchObject(youtubeInfo)
|
||||||
if (program[ 'verbose' ]) console.log('Fetched object.', videoInfo)
|
log.debug('Fetched object.', videoInfo)
|
||||||
|
|
||||||
if (program[ 'since' ]) {
|
if (program[ 'since' ]) {
|
||||||
if (buildOriginallyPublishedAt(videoInfo).getTime() < program[ 'since' ].getTime()) {
|
if (buildOriginallyPublishedAt(videoInfo).getTime() < program[ 'since' ].getTime()) {
|
||||||
console.log('Video "%s" has been published before "%s", don\'t upload it.\n',
|
log.info('Video "%s" has been published before "%s", don\'t upload it.\n',
|
||||||
videoInfo.title, formatDate(program[ 'since' ]));
|
videoInfo.title, formatDate(program[ 'since' ]));
|
||||||
return res();
|
return res();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (program[ 'until' ]) {
|
if (program[ 'until' ]) {
|
||||||
if (buildOriginallyPublishedAt(videoInfo).getTime() > program[ 'until' ].getTime()) {
|
if (buildOriginallyPublishedAt(videoInfo).getTime() > program[ 'until' ].getTime()) {
|
||||||
console.log('Video "%s" has been published after "%s", don\'t upload it.\n',
|
log.info('Video "%s" has been published after "%s", don\'t upload it.\n',
|
||||||
videoInfo.title, formatDate(program[ 'until' ]));
|
videoInfo.title, formatDate(program[ 'until' ]));
|
||||||
return res();
|
return res();
|
||||||
}
|
}
|
||||||
|
@ -127,27 +136,27 @@ function processVideo (parameters: {
|
||||||
|
|
||||||
const result = await searchVideoWithSort(url, videoInfo.title, '-match')
|
const result = await searchVideoWithSort(url, videoInfo.title, '-match')
|
||||||
|
|
||||||
console.log('############################################################\n')
|
log.info('############################################################\n')
|
||||||
|
|
||||||
if (result.body.data.find(v => v.name === videoInfo.title)) {
|
if (result.body.data.find(v => v.name === videoInfo.title)) {
|
||||||
console.log('Video "%s" already exists, don\'t reupload it.\n', videoInfo.title)
|
log.info('Video "%s" already exists, don\'t reupload it.\n', videoInfo.title)
|
||||||
return res()
|
return res()
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = join(cwd, sha256(videoInfo.url) + '.mp4')
|
const path = join(cwd, sha256(videoInfo.url) + '.mp4')
|
||||||
|
|
||||||
console.log('Downloading video "%s"...', videoInfo.title)
|
log.info('Downloading video "%s"...', videoInfo.title)
|
||||||
|
|
||||||
const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ]
|
const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ]
|
||||||
try {
|
try {
|
||||||
const youtubeDL = await safeGetYoutubeDL()
|
const youtubeDL = await safeGetYoutubeDL()
|
||||||
youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => {
|
youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err)
|
log.error(err)
|
||||||
return res()
|
return res()
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(output.join('\n'))
|
log.info(output.join('\n'))
|
||||||
await uploadVideoOnPeerTube({
|
await uploadVideoOnPeerTube({
|
||||||
cwd,
|
cwd,
|
||||||
url,
|
url,
|
||||||
|
@ -158,7 +167,7 @@ function processVideo (parameters: {
|
||||||
return res()
|
return res()
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err.message)
|
log.error(err.message)
|
||||||
return res()
|
return res()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -217,7 +226,7 @@ async function uploadVideoOnPeerTube (parameters: {
|
||||||
fixture: videoPath
|
fixture: videoPath
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('\nUploading on PeerTube video "%s".', videoAttributes.name)
|
log.info('\nUploading on PeerTube video "%s".', videoAttributes.name)
|
||||||
|
|
||||||
let accessToken = await getAccessTokenOrDie(url, user)
|
let accessToken = await getAccessTokenOrDie(url, user)
|
||||||
|
|
||||||
|
@ -225,21 +234,20 @@ async function uploadVideoOnPeerTube (parameters: {
|
||||||
await uploadVideo(url, accessToken, videoAttributes)
|
await uploadVideo(url, accessToken, videoAttributes)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.message.indexOf('401') !== -1) {
|
if (err.message.indexOf('401') !== -1) {
|
||||||
console.log('Got 401 Unauthorized, token may have expired, renewing token and retry.')
|
log.info('Got 401 Unauthorized, token may have expired, renewing token and retry.')
|
||||||
|
|
||||||
accessToken = await getAccessTokenOrDie(url, user)
|
accessToken = await getAccessTokenOrDie(url, user)
|
||||||
|
|
||||||
await uploadVideo(url, accessToken, videoAttributes)
|
await uploadVideo(url, accessToken, videoAttributes)
|
||||||
} else {
|
} else {
|
||||||
console.log(err.message)
|
exitError(err.message)
|
||||||
process.exit(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await remove(videoPath)
|
await remove(videoPath)
|
||||||
if (thumbnailfile) await remove(thumbnailfile)
|
if (thumbnailfile) await remove(thumbnailfile)
|
||||||
|
|
||||||
console.log('Uploaded video "%s"!\n', videoAttributes.name)
|
log.warn('Uploaded video "%s"!\n', videoAttributes.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------- */
|
/* ---------------------------------------------------------- */
|
||||||
|
@ -355,20 +363,17 @@ async function getAccessTokenOrDie (url: string, user: UserInfo) {
|
||||||
const res = await login(url, client, user)
|
const res = await login(url, client, user)
|
||||||
return res.body.access_token
|
return res.body.access_token
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Cannot authenticate. Please check your username/password.')
|
exitError('Cannot authenticate. Please check your username/password.')
|
||||||
process.exit(-1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDate (dateAsStr: string): Date {
|
function parseDate (dateAsStr: string): Date {
|
||||||
if (!/\d{4}-\d{2}-\d{2}/.test(dateAsStr)) {
|
if (!/\d{4}-\d{2}-\d{2}/.test(dateAsStr)) {
|
||||||
console.error(`Invalid date passed: ${dateAsStr}. Expected format: YYYY-MM-DD. See help for usage.`);
|
exitError(`Invalid date passed: ${dateAsStr}. Expected format: YYYY-MM-DD. See help for usage.`);
|
||||||
process.exit(-1);
|
|
||||||
}
|
}
|
||||||
const date = new Date(dateAsStr);
|
const date = new Date(dateAsStr);
|
||||||
if (isNaN(date.getTime())) {
|
if (isNaN(date.getTime())) {
|
||||||
console.error(`Invalid date passed: ${dateAsStr}. See help for usage.`);
|
exitError(`Invalid date passed: ${dateAsStr}. See help for usage.`);
|
||||||
process.exit(-1);
|
|
||||||
}
|
}
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
@ -376,3 +381,9 @@ function parseDate (dateAsStr: string): Date {
|
||||||
function formatDate (date: Date): string {
|
function formatDate (date: Date): string {
|
||||||
return date.toISOString().split('T')[0];
|
return date.toISOString().split('T')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function exitError (message:string, ...meta: any[]) {
|
||||||
|
// use console.error instead of log.error here
|
||||||
|
console.error(message, ...meta)
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue