1 | 'use strict'
|
2 |
|
3 | const { EventEmitter } = require('events')
|
4 | const horizon = require('horizon-youtube-mp3')
|
5 | const fue = require('file-utils-easy')
|
6 | const ytpl = require('ytpl')
|
7 | const path = require('path')
|
8 |
|
9 | const Rxfrom = require('rxjs').from
|
10 | const { mergeMap, toArray } = require('rxjs/operators')
|
11 |
|
12 | const YOUTUBE_URL = 'http://youtube.com/watch?v='
|
13 |
|
14 | class 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 |
|
29 |
|
30 |
|
31 | set fileNameGenerator (generator) {
|
32 | this.nameGenerator = generator
|
33 | }
|
34 |
|
35 | |
36 |
|
37 |
|
38 |
|
39 |
|
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 |
|
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(() => { })
|
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 |
|
103 |
|
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
|
159 | })
|
160 | })
|
161 | })
|
162 | }
|
163 | }
|
164 |
|
165 | module.exports = DownloadYTAudio
|