UNPKG

5.15 kBJavaScriptView Raw
1'use strict'
2
3const { EventEmitter } = require('events')
4const horizon = require('horizon-youtube-mp3')
5const fue = require('file-utils-easy')
6const ytpl = require('ytpl')
7const path = require('path')
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 (config = {}) {
16 super()
17 this.downloadPath = config.outputPath || __dirname
18 this.overwrite = config.overwrite || true
19 this.nameGenerator = config.fileNameGenerator || (title => `${title.replace(/[\\/:*?"<>|]/g, '')}.mp3`)
20 this.maxParallelDownload = config.maxParallelDownload || 10
21 }
22
23 set overwriteExistingFiles (overwrite) {
24 this.overwrite = overwrite
25 }
26
27 /**
28 * @param {function} generator the function must return a string, in input there will be the title of the video.
29 * This will be called only if the file name of the output file is not set
30 */
31 set fileNameGenerator (generator) {
32 this.nameGenerator = generator
33 }
34
35 /**
36 *
37 * @param {string} id the video from which to extrapolate the sound
38 * @param {!string} fileName optinal filename output
39 * @returns {Promise<object>} metadata of the mp3 file generated
40 */
41 download (id, inputFileName = null) {
42 return new Promise((resolve, reject) => {
43 let reference
44
45 const resolveFileName = (fileName) => {
46 return new Promise((resolve, reject) => {
47 if (fileName === null) {
48 // Get video id details (if needed)
49 this.getVideoInfo(id)
50 .then((songInfo) => {
51 reference = songInfo
52 resolve(this.nameGenerator(songInfo.title))
53 })
54 .catch(reject)
55 } else {
56 resolve(fileName)
57 }
58 })
59 }
60
61 resolveFileName(inputFileName)
62 .then((fileName) => {
63 const savePath = path.join(this.downloadPath, fileName)
64 return fue.existFile(savePath)
65 .catch(() => { /** the file doesn't exists, all is fine */ })
66 .then((filePath) => {
67 if (filePath && this.overwrite !== true) {
68 this.emit('error', { id, fileName, path: this.downloadPath, result: 'already exists' })
69 throw new Error(`The file ${savePath} already exists`)
70 }
71 return fileName
72 })
73 })
74 .then((fileName) => {
75 const info = { id, fileName, path: this.downloadPath, ref: reference }
76 this.emit('start', info)
77
78 horizon.downloadToLocal(`${YOUTUBE_URL}${id}`,
79 this.downloadPath, fileName, null, null,
80 (err, result) => {
81 info.result = result
82 if (err) {
83 info.result = err
84 this.emit('error', info)
85 reject(info)
86 return
87 }
88 this.emit('complete', info)
89 resolve(info)
90 },
91 (percent, timemark, targetSize) => {
92 const update = { ...info, percent, timemark, targetSize }
93 this.emit('progress', update)
94 })
95 })
96 .catch(reject)
97 })
98 }
99
100 /**
101 *
102 * @param {string} playlistId the id of the playlist to extract the sounds
103 * @returns {Promise<[object]>} the results of the downloading, will contains errors and success
104 */
105 async downloadPlaylist (playlistId) {
106 const playlistInfo = await this.getPlaylistInfo(playlistId)
107
108 const downloadingSongs = async (song) => {
109 try {
110 const songFileName = this.nameGenerator(song.title)
111 const songFile = await this.download(song.id, songFileName)
112 songFile.ref = song
113 return songFile
114 } catch (error) {
115 const quiteError = {
116 id: song.id,
117 ref: song,
118 error
119 }
120 return quiteError
121 }
122 }
123
124 const videoObservable = Rxfrom(playlistInfo.items)
125
126 return videoObservable
127 .pipe(mergeMap(video => downloadingSongs(video), this.maxParallelDownload))
128 .pipe(toArray())
129 .toPromise()
130 }
131
132 getPlaylistInfo (playlistId) {
133 return new Promise((resolve, reject) => {
134 ytpl(playlistId, (err, playlist) => {
135 if (err) {
136 return reject(err)
137 }
138 resolve(playlist)
139 })
140 })
141 }
142
143 getVideoInfo (videoId) {
144 return new Promise((resolve, reject) => {
145 horizon.getInfo(`${YOUTUBE_URL}${videoId}`, function (err, info) {
146 if (err) {
147 reject(new Error(err))
148 return
149 }
150
151 resolve({
152 id: videoId,
153 url: info.videoFile,
154 url_simple: `${YOUTUBE_URL}${videoId}`,
155 title: info.videoName,
156 thumbnail: info.videoThumb,
157 duration: info.videoTime,
158 author: null // TODO: need to improve/contribute to horizon lib
159 })
160 })
161 })
162 }
163}
164
165module.exports = DownloadYTAudio