PeerTube/server/tools/peertube-import-videos.ts
mj-saunders e291096f78
Apply import interval only when reasonable (#4552)
* Apply import interval only when reasonable

When importing videos from another service, an interval can be applied
between each download.
It only really makes sense to apply this interval when the last
attempted download actually happened, and not when it was skipped.

* Fix boolean notation
2021-11-22 16:10:00 +01:00

346 lines
9.7 KiB
TypeScript

import { registerTSPaths } from '../helpers/register-ts-paths'
registerTSPaths()
import { program } from 'commander'
import { accessSync, constants } from 'fs'
import { remove } from 'fs-extra'
import { join } from 'path'
import { sha256 } from '../helpers/core-utils'
import { doRequestAndSaveToFile } from '../helpers/requests'
import {
assignToken,
buildCommonVideoOptions,
buildServer,
buildVideoAttributesFromCommander,
getLogger,
getServerCredentials
} from './cli'
import { wait } from '@shared/extra-utils'
import { YoutubeDLCLI, YoutubeDLInfo, YoutubeDLInfoBuilder } from '@server/helpers/youtube-dl'
import prompt = require('prompt')
const processOptions = {
maxBuffer: Infinity
}
let command = program
.name('import-videos')
command = buildCommonVideoOptions(command)
command
.option('-u, --url <url>', 'Server url')
.option('-U, --username <username>', 'Username')
.option('-p, --password <token>', 'Password')
.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('--until <until>', 'Publication date (inclusive) until which the videos can be imported (YYYY-MM-DD)', parseDate)
.option('--first <first>', 'Process first n elements of returned playlist')
.option('--last <last>', 'Process last n elements of returned playlist')
.option('--wait-interval <waitInterval>', 'Duration between two video imports (in seconds)', convertIntoMs)
.option('-T, --tmpdir <tmpdir>', 'Working directory', __dirname)
.usage("[global options] [ -- youtube-dl options]")
.parse(process.argv)
const options = command.opts()
const log = getLogger(options.verbose)
getServerCredentials(command)
.then(({ url, username, password }) => {
if (!options.targetUrl) {
exitError('--target-url field is required.')
}
try {
accessSync(options.tmpdir, constants.R_OK | constants.W_OK)
} catch (e) {
exitError('--tmpdir %s: directory does not exist or is not accessible', options.tmpdir)
}
url = normalizeTargetUrl(url)
options.targetUrl = normalizeTargetUrl(options.targetUrl)
run(url, username, password)
.catch(err => exitError(err))
})
.catch(err => console.error(err))
async function run (url: string, username: string, password: string) {
if (!password) password = await promptPassword()
const youtubeDLBinary = await YoutubeDLCLI.safeGet()
let info = await getYoutubeDLInfo(youtubeDLBinary, options.targetUrl, command.args)
if (!Array.isArray(info)) info = [ info ]
// Try to fix youtube channels upload
const uploadsObject = info.find(i => !i.ie_key && !i.duration && i.title === 'Uploads')
if (uploadsObject) {
console.log('Fixing URL to %s.', uploadsObject.url)
info = await getYoutubeDLInfo(youtubeDLBinary, uploadsObject.url, command.args)
}
let infoArray: any[]
infoArray = [].concat(info)
if (options.first) {
infoArray = infoArray.slice(0, options.first)
} else if (options.last) {
infoArray = infoArray.slice(-options.last)
}
log.info('Will download and upload %d videos.\n', infoArray.length)
let skipInterval = true
for (const [ index, info ] of infoArray.entries()) {
try {
if (index > 0 && options.waitInterval && !skipInterval) {
log.info("Wait for %d seconds before continuing.", options.waitInterval / 1000)
await wait(options.waitInterval)
}
skipInterval = await processVideo({
cwd: options.tmpdir,
url,
username,
password,
youtubeInfo: info
})
} catch (err) {
console.error('Cannot process video.', { info, url, err })
}
}
log.info('Video/s for user %s imported: %s', username, options.targetUrl)
process.exit(0)
}
async function processVideo (parameters: {
cwd: string
url: string
username: string
password: string
youtubeInfo: any
}) {
const { youtubeInfo, cwd, url, username, password } = parameters
log.debug('Fetching object.', youtubeInfo)
const videoInfo = await fetchObject(youtubeInfo)
log.debug('Fetched object.', videoInfo)
if (options.since && videoInfo.originallyPublishedAt && videoInfo.originallyPublishedAt.getTime() < options.since.getTime()) {
log.info('Video "%s" has been published before "%s", don\'t upload it.\n', videoInfo.name, formatDate(options.since))
return true
}
if (options.until && videoInfo.originallyPublishedAt && videoInfo.originallyPublishedAt.getTime() > options.until.getTime()) {
log.info('Video "%s" has been published after "%s", don\'t upload it.\n', videoInfo.name, formatDate(options.until))
return true
}
const server = buildServer(url)
const { data } = await server.search.advancedVideoSearch({
search: {
search: videoInfo.name,
sort: '-match',
searchTarget: 'local'
}
})
log.info('############################################################\n')
if (data.find(v => v.name === videoInfo.name)) {
log.info('Video "%s" already exists, don\'t reupload it.\n', videoInfo.name)
return true
}
const path = join(cwd, sha256(videoInfo.url) + '.mp4')
log.info('Downloading video "%s"...', videoInfo.name)
try {
const youtubeDLBinary = await YoutubeDLCLI.safeGet()
const output = await youtubeDLBinary.download({
url: videoInfo.url,
format: YoutubeDLCLI.getYoutubeDLVideoFormat([]),
output: path,
additionalYoutubeDLArgs: command.args,
processOptions
})
log.info(output.join('\n'))
await uploadVideoOnPeerTube({
cwd,
url,
username,
password,
videoInfo,
videoPath: path
})
} catch (err) {
log.error(err.message)
}
return false
}
async function uploadVideoOnPeerTube (parameters: {
videoInfo: YoutubeDLInfo
videoPath: string
cwd: string
url: string
username: string
password: string
}) {
const { videoInfo, videoPath, cwd, url, username, password } = parameters
const server = buildServer(url)
await assignToken(server, username, password)
let thumbnailfile: string
if (videoInfo.thumbnailUrl) {
thumbnailfile = join(cwd, sha256(videoInfo.thumbnailUrl) + '.jpg')
await doRequestAndSaveToFile(videoInfo.thumbnailUrl, thumbnailfile)
}
const baseAttributes = await buildVideoAttributesFromCommander(server, program, videoInfo)
const attributes = {
...baseAttributes,
originallyPublishedAt: videoInfo.originallyPublishedAt
? videoInfo.originallyPublishedAt.toISOString()
: null,
thumbnailfile,
previewfile: thumbnailfile,
fixture: videoPath
}
log.info('\nUploading on PeerTube video "%s".', attributes.name)
try {
await server.videos.upload({ attributes })
} catch (err) {
if (err.message.indexOf('401') !== -1) {
log.info('Got 401 Unauthorized, token may have expired, renewing token and retry.')
server.accessToken = await server.login.getAccessToken(username, password)
await server.videos.upload({ attributes })
} else {
exitError(err.message)
}
}
await remove(videoPath)
if (thumbnailfile) await remove(thumbnailfile)
log.info('Uploaded video "%s"!\n', attributes.name)
}
/* ---------------------------------------------------------- */
async function fetchObject (info: any) {
const url = buildUrl(info)
const youtubeDLCLI = await YoutubeDLCLI.safeGet()
const result = await youtubeDLCLI.getInfo({
url,
format: YoutubeDLCLI.getYoutubeDLVideoFormat([]),
processOptions
})
const builder = new YoutubeDLInfoBuilder(result)
const videoInfo = builder.getInfo()
return { ...videoInfo, url }
}
function buildUrl (info: any) {
const webpageUrl = info.webpage_url as string
if (webpageUrl?.match(/^https?:\/\//)) return webpageUrl
const url = info.url as string
if (url?.match(/^https?:\/\//)) return url
// It seems youtube-dl does not return the video url
return 'https://www.youtube.com/watch?v=' + info.id
}
function normalizeTargetUrl (url: string) {
let normalizedUrl = url.replace(/\/+$/, '')
if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) {
normalizedUrl = 'https://' + normalizedUrl
}
return normalizedUrl
}
async function promptPassword () {
return new Promise<string>((res, rej) => {
prompt.start()
const schema = {
properties: {
password: {
hidden: true,
required: true
}
}
}
prompt.get(schema, function (err, result) {
if (err) {
return rej(err)
}
return res(result.password)
})
})
}
function parseDate (dateAsStr: string): Date {
if (!/\d{4}-\d{2}-\d{2}/.test(dateAsStr)) {
exitError(`Invalid date passed: ${dateAsStr}. Expected format: YYYY-MM-DD. See help for usage.`)
}
const date = new Date(dateAsStr)
date.setHours(0, 0, 0)
if (isNaN(date.getTime())) {
exitError(`Invalid date passed: ${dateAsStr}. See help for usage.`)
}
return date
}
function formatDate (date: Date): string {
return date.toISOString().split('T')[0]
}
function convertIntoMs (secondsAsStr: string): number {
const seconds = parseInt(secondsAsStr, 10)
if (seconds <= 0) {
exitError(`Invalid duration passed: ${seconds}. Expected duration to be strictly positive and in seconds`)
}
return Math.round(seconds * 1000)
}
function exitError (message: string, ...meta: any[]) {
// use console.error instead of log.error here
console.error(message, ...meta)
process.exit(-1)
}
function getYoutubeDLInfo (youtubeDLCLI: YoutubeDLCLI, url: string, args: string[]) {
return youtubeDLCLI.getInfo({
url,
format: YoutubeDLCLI.getYoutubeDLVideoFormat([]),
additionalYoutubeDLArgs: [ '-j', '--flat-playlist', '--playlist-reverse', ...args ],
processOptions
})
}