UNPKG

4.28 kBJavaScriptView Raw
1'use strict'
2
3const { once, EventEmitter } = require('events')
4const path = require('path')
5const ytdl = require('ytdl-core') // single video
6const ytpl = require('ytpl') // playlist
7const ffmpeg = require('fluent-ffmpeg')
8
9const Rxfrom = require('rxjs').from
10const { mergeMap, toArray } = require('rxjs/operators')
11
12const YOUTUBE_URL = 'http://youtube.com/watch?v='
13
14class DownloadYTAudio extends EventEmitter {
15 constructor (opts = {}) {
16 super()
17 this.downloadPath = opts.outputPath || process.cwd()
18 this.nameGenerator = opts.fileNameGenerator || (title => `${title.replace(/[\\/:*?"<>|]/g, '')}.mp3`)
19 this.maxParallelDownload = opts.maxParallelDownload || 10
20
21 if (opts.ffmpegPath) {
22 process.nextTick(() => {
23 ffmpeg.setFfmpegPath(opts.ffmpegPath)
24 })
25 }
26 }
27
28 /**
29 *
30 * @param {string} id the video from which to extrapolate the sound
31 * @param {!string} fileName optinal filename output
32 * @returns {Promise<object>} metadata of the mp3 file generated
33 */
34 async download (videoId, fileName) {
35 const url = `${YOUTUBE_URL}${videoId}`
36 const videoStream = ytdl(url, {
37 quality: 'highestaudio'
38 })
39
40 // videoStream.on('progress', (a, b, c) => {
41 // console.log({ a, b, c })
42 // })
43 const [videoInfo, videoSetting] = await once(videoStream, 'info')
44
45 if (!fileName) {
46 fileName = this.nameGenerator(videoInfo.title)
47 }
48
49 const info = {
50 id: videoId,
51 fileName,
52 path: this.downloadPath,
53 filePath: path.join(this.downloadPath, fileName),
54 ref: buildVideoRef(videoInfo)
55 }
56
57 this.emit('video-info', info, videoInfo)
58 this.emit('video-setting', info, videoSetting)
59
60 let theResolve
61 let theReject
62 const thePromise = new Promise((resolve, reject) => {
63 theResolve = resolve
64 theReject = reject
65 })
66
67 const command = ffmpeg(videoStream, {
68 stdoutLines: 0
69 })
70 .format('mp3')
71 .output(info.filePath)
72 // TODO other config
73 .on('start', () => { this.emit('start', info) })
74 .on('end', () => {
75 this.emit('complete', info)
76 if (theResolve) {
77 theResolve(info)
78 theReject = null
79 }
80 })
81 .on('error', (err) => {
82 this.emit('error', err, info)
83 if (theReject) {
84 theReject(err)
85 theResolve = null
86 }
87 })
88 .on('progress', progress => {
89 const update = { ...info, progress }
90 this.emit('progress', update)
91 })
92
93 // start after the user register its events listener
94 process.nextTick(() => { command.run() })
95
96 return thePromise
97 }
98
99 /**
100 *
101 * @param {string} playlistId the id of the playlist to extract the sounds
102 * @returns {Promise<[object]>} the results of the downloading, will contains errors and success
103 */
104 async downloadPlaylist (playlistId) {
105 const playlistInfo = await this.getPlaylistInfo(playlistId)
106
107 const downloadingSongs = async (song) => {
108 try {
109 const songFileName = this.nameGenerator(song.title)
110 const songFile = await this.download(song.id, songFileName)
111 return songFile
112 } catch (error) {
113 return {
114 id: song.id,
115 ref: song,
116 error
117 }
118 }
119 }
120
121 const videoObservable = Rxfrom(playlistInfo.items)
122
123 return videoObservable
124 .pipe(mergeMap(video => downloadingSongs(video), this.maxParallelDownload))
125 .pipe(toArray())
126 .toPromise()
127 }
128
129 getPlaylistInfo (playlistId) {
130 return ytpl(playlistId)
131 }
132
133 async getVideoInfo (videoId) {
134 const url = `${YOUTUBE_URL}${videoId}`
135 const advData = await ytdl.getBasicInfo(url)
136 if (advData.formats.length === 0) {
137 // sometimes even fake id return something
138 throw new Error('This video is unavailable')
139 }
140
141 return buildVideoRef(advData)
142 }
143}
144
145module.exports = DownloadYTAudio
146
147function buildVideoRef (advData) {
148 return {
149 id: advData.video_id,
150 url: advData.video_url,
151 title: advData.title,
152 thumbnail: advData.player_response.videoDetails.thumbnail.thumbnails.reduce(getBigger),
153 duration: +(advData.player_response.videoDetails.lengthSeconds),
154 author: advData.author
155 }
156}
157
158function getBigger (a, b) {
159 return (a.width > b.width) ? a : b
160}