72223a01a288d0916da3dacc1378cc3dd6cf6f1d
[oweals/peertube.git] / server / tools / import-videos.ts
1 import * as program from 'commander'
2 import { join } from 'path'
3 import * as youtubeDL from 'youtube-dl'
4 import { VideoPrivacy } from '../../shared/models/videos'
5 import { unlinkPromise } from '../helpers/core-utils'
6 import { doRequestAndSaveToFile } from '../helpers/requests'
7 import { CONSTRAINTS_FIELDS } from '../initializers'
8 import { getClient, getVideoCategories, login, searchVideo, uploadVideo } from '../tests/utils'
9
10 program
11   .option('-u, --url <url>', 'Server url')
12   .option('-U, --username <username>', 'Username')
13   .option('-p, --password <token>', 'Password')
14   .option('-t, --target-url <targetUrl>', 'Video target URL')
15   .option('-l, --language <languageCode>', 'Language code')
16   .option('-v, --verbose', 'Verbose mode')
17   .parse(process.argv)
18
19 if (
20   !program['url'] ||
21   !program['username'] ||
22   !program['password'] ||
23   !program['targetUrl']
24 ) {
25   console.error('All arguments are required.')
26   process.exit(-1)
27 }
28
29 run().catch(err => console.error(err))
30
31 let accessToken: string
32 let client: { id: string, secret: string }
33
34 const user = {
35   username: program['username'],
36   password: program['password']
37 }
38
39 const processOptions = {
40   cwd: __dirname,
41   maxBuffer: Infinity
42 }
43
44 async function run () {
45   const res = await getClient(program['url'])
46   client = {
47     id: res.body.client_id,
48     secret: res.body.client_secret
49   }
50
51   const res2 = await login(program['url'], client, user)
52   accessToken = res2.body.access_token
53
54   const options = [ '-j', '--flat-playlist', '--playlist-reverse' ]
55   youtubeDL.getInfo(program['targetUrl'], options, processOptions, async (err, info) => {
56     if (err) {
57       console.log(err.message);
58       process.exit(1);
59     }
60
61     let infoArray: any[]
62
63     // Normalize utf8 fields
64     if (Array.isArray(info) === true) {
65       infoArray = info.map(i => normalizeObject(i))
66     } else {
67       infoArray = [ normalizeObject(info) ]
68     }
69     console.log('Will download and upload %d videos.\n', infoArray.length)
70
71     for (const info of infoArray) {
72       await processVideo(info, program['language'])
73     }
74
75     // https://www.youtube.com/watch?v=2Upx39TBc1s
76     console.log('I\'m finished!')
77     process.exit(0)
78   })
79 }
80
81 function processVideo (info: any, languageCode: number) {
82   return new Promise(async res => {
83     if (program['verbose']) console.log('Fetching object.', info)
84
85     const videoInfo = await fetchObject(info)
86     if (program['verbose']) console.log('Fetched object.', videoInfo)
87
88     const result = await searchVideo(program['url'], videoInfo.title)
89
90     console.log('############################################################\n')
91
92     if (result.body.data.find(v => v.name === videoInfo.title)) {
93       console.log('Video "%s" already exists, don\'t reupload it.\n', videoInfo.title)
94       return res()
95     }
96
97     const path = join(__dirname, new Date().getTime() + '.mp4')
98
99     console.log('Downloading video "%s"...', videoInfo.title)
100
101     const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ]
102     youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => {
103       if (err) return console.error(err)
104
105       console.log(output.join('\n'))
106
107       await uploadVideoOnPeerTube(normalizeObject(videoInfo), path, languageCode)
108
109       return res()
110     })
111   })
112 }
113
114 async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, language?: number) {
115   const category = await getCategory(videoInfo.categories)
116   const licence = getLicence(videoInfo.license)
117   let tags = []
118   if (Array.isArray(videoInfo.tags)) {
119     tags = videoInfo.tags
120       .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min)
121       .map(t => t.normalize())
122       .slice(0, 5)
123   }
124
125   let thumbnailfile
126   if (videoInfo.thumbnail) {
127     thumbnailfile = join(__dirname, 'thumbnail.jpg')
128
129     await doRequestAndSaveToFile({
130       method: 'GET',
131       uri: videoInfo.thumbnail
132     }, thumbnailfile)
133   }
134
135   const videoAttributes = {
136     name: videoInfo.title,
137     category,
138     licence,
139     language,
140     nsfw: isNSFW(videoInfo),
141     commentsEnabled: true,
142     description: videoInfo.description,
143     support: undefined,
144     tags,
145     privacy: VideoPrivacy.PUBLIC,
146     fixture: videoPath,
147     thumbnailfile,
148     previewfile: thumbnailfile
149   }
150
151   console.log('\nUploading on PeerTube video "%s".', videoAttributes.name)
152   try {
153     await uploadVideo(program['url'], accessToken, videoAttributes)
154   } catch (err) {
155     if (err.message.indexOf('401') !== -1) {
156       console.log('Got 401 Unauthorized, token may have expired, renewing token and retry.')
157
158       const res = await login(program['url'], client, user)
159       accessToken = res.body.access_token
160
161       await uploadVideo(program['url'], accessToken, videoAttributes)
162     } else {
163       console.log(err.message);
164       process.exit(1);
165     }
166   }
167
168   await unlinkPromise(videoPath)
169   if (thumbnailfile) {
170     await unlinkPromise(thumbnailfile)
171   }
172
173   console.log('Uploaded video "%s"!\n', videoAttributes.name)
174 }
175
176 async function getCategory (categories: string[]) {
177   if (!categories) return undefined
178
179   const categoryString = categories[0]
180
181   if (categoryString === 'News & Politics') return 11
182
183   const res = await getVideoCategories(program['url'])
184   const categoriesServer = res.body
185
186   for (const key of Object.keys(categoriesServer)) {
187     const categoryServer = categoriesServer[key]
188     if (categoryString.toLowerCase() === categoryServer.toLowerCase()) return parseInt(key, 10)
189   }
190
191   return undefined
192 }
193
194 function getLicence (licence: string) {
195   if (!licence) return undefined
196
197   if (licence.indexOf('Creative Commons Attribution licence') !== -1) return 1
198
199   return undefined
200 }
201
202 function normalizeObject (obj: any) {
203   const newObj: any = {}
204
205   for (const key of Object.keys(obj)) {
206     // Deprecated key
207     if (key === 'resolution') continue
208
209     const value = obj[key]
210
211     if (typeof value === 'string') {
212       newObj[key] = value.normalize()
213     } else {
214       newObj[key] = value
215     }
216   }
217
218   return newObj
219 }
220
221 function fetchObject (info: any) {
222   const url = buildUrl(info)
223
224   return new Promise<any>(async (res, rej) => {
225     youtubeDL.getInfo(url, undefined, processOptions, async (err, videoInfo) => {
226       if (err) return rej(err)
227
228       const videoInfoWithUrl = Object.assign(videoInfo, { url })
229       return res(normalizeObject(videoInfoWithUrl))
230     })
231   })
232 }
233
234 function buildUrl (info: any) {
235   const webpageUrl = info.webpage_url as string
236   if (webpageUrl && webpageUrl.match(/^https?:\/\//)) return webpageUrl
237
238   const url = info.url as string
239   if (url && url.match(/^https?:\/\//)) return url
240
241   // It seems youtube-dl does not return the video url
242   return 'https://www.youtube.com/watch?v=' + info.id
243 }
244
245 function isNSFW (info: any) {
246   if (info.age_limit && info.age_limit >= 16) return true
247
248   return false
249 }