├── Aptfile ├── Procfile ├── .eslintrc ├── .travis.yml ├── .buildpacks ├── .gitignore ├── sources.list ├── src ├── ascii-art.txt ├── test │ ├── index.js │ ├── server.js │ ├── metadata.js │ ├── search.js │ ├── search-set-key.js │ ├── download.js │ └── set-folder.js ├── index.html ├── bin.js ├── downloader.js ├── index.js └── youtube.js ├── Dockerfile ├── .editorconfig ├── package.json ├── CHANGELOG.md └── README.md /Aptfile: -------------------------------------------------------------------------------- 1 | ffmpeg 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node src/bin.js 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard" 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" -------------------------------------------------------------------------------- /.buildpacks: -------------------------------------------------------------------------------- 1 | https://github.com/heroku/heroku-buildpack-apt 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | *.json 4 | *.mp3 5 | !package.json 6 | !package-lock.json 7 | -------------------------------------------------------------------------------- /sources.list: -------------------------------------------------------------------------------- 1 | deb http://ppa.launchpad.net/openjdk-r/ppa/ubuntu trusty main 2 | deb-src http://ppa.launchpad.net/openjdk-r/ppa/ubuntu trusty main -------------------------------------------------------------------------------- /src/ascii-art.txt: -------------------------------------------------------------------------------- 1 | _ _ __ _ ___ 2 | | | | |/ _` / __| 3 | | |_| | (_| \__ \ 4 | \__, |\__,_|___/ 5 | |___/ 6 | 7 | youtube-audio-server 8 | 9 | -------------------------------------------------------------------------------- /src/test/index.js: -------------------------------------------------------------------------------- 1 | require('./download') 2 | require('./metadata') 3 | require('./search-set-key') 4 | require('./search') 5 | require('./set-folder') 6 | require('./server') 7 | -------------------------------------------------------------------------------- /src/test/server.js: -------------------------------------------------------------------------------- 1 | const yas = require('../index') 2 | 3 | // Start listener (REST API). 4 | const port = 7331 5 | yas.listen(port, () => { 6 | console.log(`Listening on port http://localhost:${port}.`) 7 | }) 8 | -------------------------------------------------------------------------------- /src/test/metadata.js: -------------------------------------------------------------------------------- 1 | const yas = require('../index') 2 | 3 | // Get metadata. 4 | yas.get('HQmmM_qwG4k', (err, data) => { 5 | console.log('-'.repeat(80)) 6 | console.log('GOT METADATA for HQmmM_qwG4k:', data || err) 7 | }) 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jrottenberg/ffmpeg:3.4-alpine 2 | FROM node:10-alpine 3 | 4 | # copy ffmpeg bins from first image 5 | COPY --from=0 / / 6 | 7 | WORKDIR /usr/src/yas 8 | RUN npm i -g youtube-audio-server 9 | EXPOSE 80 10 | CMD yas 11 | -------------------------------------------------------------------------------- /src/test/search.js: -------------------------------------------------------------------------------- 1 | const yas = require('../index') 2 | 3 | // Search. 4 | yas.search( 5 | { 6 | query: 'led zeppelin', 7 | page: null 8 | }, 9 | (err, data) => { 10 | console.log('-'.repeat(80)) 11 | if (err) { 12 | console.log('ERROR:', err) 13 | return 14 | } 15 | 16 | console.log(`FIRST SEARCH RESULT of ${data.items.length}:`, data.items[0]) 17 | } 18 | ) 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | 17 | # Indentation override 18 | #[lib/**.js] 19 | #[{package.json,.travis.yml}] 20 | #[**/**.js] 21 | -------------------------------------------------------------------------------- /src/test/search-set-key.js: -------------------------------------------------------------------------------- 1 | const yas = require('../index') 2 | 3 | // Programatic search. 4 | const apiKey = 'YOUR-KEY-HERE' 5 | yas.setKey(apiKey) 6 | yas.search( 7 | { 8 | query: 'led zeppelin', 9 | page: null 10 | }, 11 | (err, data) => { 12 | console.log('-'.repeat(80)) 13 | if (err) { 14 | console.log('ERROR:', err) 15 | return 16 | } 17 | 18 | console.log(`FIRST SEARCH RESULT of ${data.items.length}:`, data.items[0]) 19 | } 20 | ) 21 | -------------------------------------------------------------------------------- /src/test/download.js: -------------------------------------------------------------------------------- 1 | const yas = require('../index') 2 | 3 | // Download audio. 4 | const id = 'HQmmM_qwG4k' // "Whole Lotta Love" by Led Zeppelin. 5 | const file = 'whole-lotta-love.mp3' 6 | console.log(`Downloading ${id} into ${file}...`) 7 | yas.downloader 8 | .onSuccess(({ id, file }) => { 9 | console.log('-'.repeat(80)) 10 | console.log(`Yay! Audio from ${id} downloaded successfully into "${file}"!`) 11 | }) 12 | .onError(({ id, file, error }) => { 13 | console.log('-'.repeat(80)) 14 | console.error(`Sorry, an error ocurred when trying to download ${id}`, error) 15 | }) 16 | .download({ id, file }) 17 | -------------------------------------------------------------------------------- /src/test/set-folder.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const yas = require('../index') 4 | 5 | // Download audio on set folder. 6 | const id = 'HQmmM_qwG4k' // "Whole Lotta Love" by Led Zeppelin. 7 | const folder = path.resolve(__dirname, 'temp-audio') 8 | const file = `${folder}/whole-lotta-love.mp3` 9 | console.log(`Downloading ${id} into ${file}...`) 10 | 11 | yas.downloader 12 | .setFolder(folder) 13 | .onSuccess(({ id, file }) => { 14 | console.log('-'.repeat(80)) 15 | console.log(`Yay! Audio from ${id} downloaded successfully into "${file}"!`) 16 | }) 17 | .onError(({ id, file, error }) => { 18 | console.log('-'.repeat(80)) 19 | console.error(`Sorry, an error ocurred when trying to download ${id}`, error) 20 | }) 21 | .download({ id, file }) 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "youtube-audio-server", 3 | "version": "2.8.2", 4 | "description": "Easily stream and download audio from YouTube.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "./src/bin.js", 8 | "test": "standard", 9 | "test-focus": "standard-focus", 10 | "test-run": "node src/test/index.js" 11 | }, 12 | "bin": { 13 | "yas": "./src/bin.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/codealchemist/youtube-audio-server.git" 18 | }, 19 | "author": "Alberto Miranda", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/codealchemist/youtube-audio-server/issues" 23 | }, 24 | "homepage": "https://github.com/codealchemist/youtube-audio-server#readme", 25 | "dependencies": { 26 | "chalk": "^1.1.3", 27 | "download": "^8.0.0", 28 | "express": "^4.17.1", 29 | "express-no-favicons": "0.0.1", 30 | "fluent-ffmpeg": "^2.1.2", 31 | "minimist": "^1.2.0", 32 | "mkdirp": "^0.5.1", 33 | "ora": "^5.4.1", 34 | "sanitize-filename": "^1.6.3", 35 | "through2": "^2.0.3", 36 | "youtube-node": "^1.3.3", 37 | "ytdl-core": "^4.9.1" 38 | }, 39 | "devDependencies": { 40 | "standard-focus": "^1.1.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # youtube-audio-server changelog 2 | 3 | ## v2.8.2 4 | 5 | ### Removed 6 | 7 | - Removed engines from package.json. 8 | 9 | ## v2.8.1 10 | 11 | ### Fixed 12 | 13 | - Using folder configured with `setFolder` to save temp files. 14 | 15 | ## v2.8.0 16 | 17 | ### Added 18 | 19 | - `setFolder` to optionally set the folder were content will be downloaded. 20 | 21 | ## v2.7.1 22 | 23 | ### Fixed 24 | 25 | - Handling temp file errors 26 | 27 | ## v2.7.0 28 | 29 | ### Added 30 | 31 | - `onMetadata` callback for `downloader` 32 | - Returning `filename` on `onSuccess` response 33 | 34 | ## v2.6.1 35 | 36 | ### Fixed 37 | 38 | - Avoid saving empty metadata 39 | 40 | ## v2.6.0 41 | 42 | ### Added 43 | 44 | - metadata support 45 | 46 | ## v2.3.0 47 | 48 | ### Added 49 | 50 | - /cache/[videoId] endpoint: Returns the same stream for requested audio 51 | until processing finishes. 52 | - `/chunk/[videoId]`: Saves mp3 file to disk and returns a stream to it 53 | with chunks supports. 54 | 55 | ### Fixed 56 | 57 | - Killing ffmpeg when processing finishes. 58 | 59 | ## v2.2.0 60 | 61 | ### Added 62 | 63 | - setKey method to allow setting YouTube API key programatically. 64 | 65 | ## v2.1.5 66 | 67 | ### Fixed 68 | 69 | - Bug #12: Downloading video file instead of just audio. 70 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |Welcome to
52 |
10 |
11 | ## Install
12 |
13 | `npm install -g youtube-audio-server`
14 |
15 | Or:
16 |
17 | `npm install --save youtube-audio-server`
18 |
19 | ## Docker image
20 |
21 | https://hub.docker.com/r/codealchemist/youtube-audio-server
22 |
23 | ## Search and metadata
24 |
25 | **IMPORTANT:** To be able to search and get video metadata you need to start the app passing your
26 | Google App KEY.
27 |
28 | Your Google App needs to have the YouTube API enabled.
29 |
30 | Login at https://console.cloud.google.com to get this data.
31 |
32 | To support this features, _YAS_ should be started like this:
33 |
34 | `KEY=[YOUR-APP-KEY] yas`
35 |
36 | If you use **YAS** programmatically you need to ensure the `KEY` environment var
37 | is set, or since version 2.2.0 you can also set it using the `setKey` method:
38 |
39 | ```
40 | const yas = require('youtube-audio-server')
41 | yas.setKey('YOUR-KEY')
42 | ```
43 |
44 | ## Running on Heroku
45 |
46 | To be able to run **YAS** on Heroku you need to install the **ffmpeg** buildpack:
47 |
48 | `heroku buildpacks:add https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest.git`
49 |
50 | ## Command line usage
51 |
52 | ### REST API
53 |
54 | Start **YAS** with `yas`.
55 |
56 | #### Audio stream
57 |
58 | Just hit the server passing a YouTube video id, like:
59 |
60 | http://yourServerAddress:port/[videoId]
61 |
62 | For example:
63 |
64 | http://localhost:4000/HQmmM_qwG4k
65 |
66 | This will stream the requested video's audio.
67 |
68 | You can play it on an HTML5 audio tag or however you like.
69 |
70 | **Other endpoints:**
71 |
72 | - `/cache/[videoId]`: Returns the same stream for requested audio
73 | until processing finishes. Useful to avoid multiple requests from creating
74 | zombie instances of ffmpeg. This happens in Chrome, which makes a document
75 | request first and then a media request. The document request makes ffmpeg
76 | to start processing but never finishes.
77 | Firefox properly loads the audio with just one request and allows seeking.
78 | - `/chunk/[videoId]`: Saves mp3 file to disk and returns a stream to it.
79 | This allows data chunks to be sent to the client, which will be able to seek
80 | across the file. Enables Chrome and VLC, for example, to do seeking.
81 |
82 | #### Get metadata
83 |
84 | Use: http://yourServerAddress:port/get/[videoId]
85 |
86 | #### Search
87 |
88 | Use: http://yourServerAddress:port/search/[query]/[[pageToken]]
89 |
90 | To navigate pages you need to use `pageToken` which is provided in the results on the
91 | root level property `nextPageToken`.
92 |
93 | ### Change port:
94 |
95 | Default is 80.
96 |
97 | You can easily change it by starting **YAS** like:
98 |
99 | `PORT=8080 yas`
100 |
101 | Or, you can set the port using args:
102 |
103 | `yas -p 8080` or `yas --port 8080`
104 |
105 | ### Download audio
106 |
107 | **YAS** can also be used to easliy download audio.
108 |
109 | In this mode, the server is not started.
110 |
111 | **Usage:**
112 |
113 | `yas --id [youtube-video-id|youtube-video-url] [--file [./sample.mp3]]`
114 |
115 | **With metadata:**
116 |
117 | `yas --id 2zYDMN4h2hY -m`
118 |
119 | Use `-m` or `--metadata` to retrieve and persist metadata as ID3 tags, naming your file with the video title by default.
120 |
121 | Saved ID3 tags:
122 |
123 | - title
124 | - description
125 | - artist
126 | - album
127 | - comment: video URL
128 |
129 | **Other examples:**
130 |
131 | ```
132 | yas --id 2zYDMN4h2hY --file ~/Downloads/Music/sample.mp3
133 | yas --id 2zYDMN4h2hY
134 | yas --id https://www.youtube.com/watch?v=2zYDMN4h2hY
135 | ```
136 |
137 | **NOTE:**
138 |
139 | FILE defaults to `./[videoId].mp3` when not set.
140 |
141 | **Alternative method:**
142 |
143 | If you have a server instance running and you want to use it to download audio,
144 | you can do this:
145 |
146 | `curl [your-server-url]/[youtube-video-id] > sample.mp3`
147 |
148 | ## Programatic usage
149 |
150 | Yeah, you can also include **YAS** in your project and use it programatically!
151 |
152 | ### REST API
153 |
154 | ```
155 | const yas = require('youtube-audio-server')
156 |
157 | // Start listener (REST API).
158 | const port = 7331
159 | yas.listen(port, () => {
160 | console.log(`Listening on port http://localhost:${port}.`)
161 | })
162 |
163 | ```
164 |
165 | ### Download audio
166 |
167 | ```
168 | const yas = require('youtube-audio-server')
169 |
170 | const id = 'HQmmM_qwG4k' // "Whole Lotta Love" by Led Zeppelin.
171 | const file = 'whole-lotta-love.mp3'
172 | console.log(`Downloading ${id} into ${file}...`)
173 | yas.downloader
174 | .setFolder('some/folder') // Optionally set a folder for downloaded content.
175 | .onSuccess(({id, file}) => {
176 | console.log(`Yay! Audio (${id}) downloaded successfully into "${file}"!`)
177 | })
178 | .onError(({ id, file, error }) => {
179 | console.error(`Sorry, an error ocurred when trying to download ${id}`, error)
180 | })
181 | .download({ id, file, cache, metadata })
182 | ```
183 |
184 | Params:
185 |
186 | - `id`: Video ID or URL (`HQmmM_qwG4k` or `https://www.youtube.com/watch?v=HQmmM_qwG4k`)
187 | - `file`: Output file; defaults to video id or title when `metadata` is true
188 | - `cache`: Use cache
189 | - `metadata`: Retrieve and set metadata as ID3 tags
190 |
191 | ### Get video metadata
192 |
193 | ```
194 | const yas = require('youtube-audio-server')
195 |
196 | yas.get('HQmmM_qwG4k', (err, data) => {
197 | console.log('GOT METADATA for HQmmM_qwG4k:', data || err)
198 | })
199 | ```
200 |
201 | ### Search
202 |
203 | ```
204 | const yas = require('youtube-audio-server')
205 |
206 | yas.search({
207 | query: 'led zeppelin',
208 | page: null
209 | },
210 | (err, data) => {
211 | console.log('RESULTS:', data || err)
212 | })
213 | ```
214 |
215 | To navigate pages you need to use `pageToken` which is provided in the results on the
216 | root level property `data.nextPageToken`.
217 |
218 | ## Dependencies
219 |
220 | The key dependency for _youtube-audio-server_ is
221 | [youtube-audio-stream](https://github.com/JamesKyburz/youtube-audio-stream),
222 | which depends on `ffmpeg`, which must be installed at system level, it's not
223 | a node dependency!
224 |
225 | ### Install ffmpeg on OSX
226 |
227 | `brew install ffmpeg`
228 |
229 | ### Install ffmpeg on Debian Linux
230 |
231 | `sudo apt-get install ffmpeg`
232 |
233 | ## Testing
234 |
235 | Just open the URL of your server instance without specifying a video id.
236 |
237 | This will load a test page with an HTML5 audio element that will stream a test video id.
238 |
239 | Run `npm test` to lint everything using [StandardJS](https://standardjs.com).
240 |
241 | To start the listener and download an audio file use `npm run test-run`.
242 |
243 | You can open the shown URL to test the REST API works as expected.
244 |
245 | You can also use `npm run test-focus` to concentrate on one linting
246 | issue at a time with the help of [standard-focus](https://www.npmjs.com/package/standard-focus).
247 |
248 | Enjoy!
249 |
--------------------------------------------------------------------------------
/src/youtube.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 | const { white, yellow, gray, red } = require('chalk')
4 | const ytdl = require('ytdl-core')
5 | const YtNode = require('youtube-node')
6 | const through2 = require('through2')
7 | const Ffmpeg = require('fluent-ffmpeg')
8 | const download = require('download')
9 | const sanitize = require('sanitize-filename')
10 | const ora = require('ora')
11 | const spinner = ora()
12 | const mkdirp = require('mkdirp')
13 | const cache = {}
14 |
15 | class YouTube {
16 | constructor () {
17 | this.pageSize = 10
18 | this.audioFolder = path.resolve('.')
19 |
20 | const envApiKey = process.env.KEY
21 | if (envApiKey) this.setKey(envApiKey)
22 | }
23 |
24 | setFolder(folder) {
25 | try {
26 | mkdirp.sync(folder)
27 | console.log('Using audio folder:', folder)
28 | this.audioFolder = folder
29 | } catch (error) {
30 | console.log(`Error creating folder: ${folder}`, error)
31 | }
32 | }
33 |
34 | setKey (apiKey) {
35 | this.ytNode = new YtNode()
36 | this.ytNode.setKey(apiKey)
37 | }
38 |
39 | streamDownloaded (id, callback) {
40 | const video = ytdl(id)
41 | const ffmpeg = new Ffmpeg(video)
42 | let sent = false
43 |
44 | try {
45 | const file = `${this.audioFolder}/${id}.mp3`
46 | ffmpeg
47 | .format('mp3')
48 | .on('end', () => {
49 | ffmpeg.kill()
50 | })
51 | .on('data', () => {
52 | if (sent) return
53 | sent = true
54 | callback(fs.createReadStream(file))
55 | })
56 | .save(file)
57 | } catch (e) {
58 | throw e
59 | }
60 | }
61 |
62 | async getMetadata (id) {
63 | const { videoDetails } = await ytdl.getBasicInfo(id)
64 | const {
65 | videoId,
66 | title,
67 | description,
68 | // thumbnails,
69 | video_url,
70 | media
71 | } = videoDetails
72 | // const imgUrl = thumbnails?.length ? thumbnails[0]?.url : ''
73 | const imgUrl = `https://img.youtube.com/vi/${videoId}/0.jpg`
74 | const { song, category, artist, album } = media || {}
75 | console.log(`${white('ᐧ Title:')} ${yellow(title)}`)
76 |
77 | return {
78 | videoId,
79 | title,
80 | description,
81 | song,
82 | category,
83 | artist,
84 | album,
85 | videoUrl: video_url,
86 | imgUrl
87 | }
88 | }
89 |
90 | setNonEmptyMetadataProp (ffmpeg, prop, value) {
91 | if (!value) return
92 | ffmpeg.outputOptions('-metadata', `${prop}="${value}"`)
93 | }
94 |
95 | setMetadata ({ file, id, metadata }) {
96 | return new Promise(async (resolve, reject) => {
97 | const ffmpeg = new Ffmpeg(file)
98 | const tmpFile = `${file}.tmp.mp3`
99 | const {
100 | videoId,
101 | title,
102 | description,
103 | artist,
104 | album,
105 | videoUrl,
106 | imgUrl
107 | } = metadata
108 |
109 | this.setNonEmptyMetadataProp(ffmpeg, 'title', title)
110 | this.setNonEmptyMetadataProp(ffmpeg, 'description', description)
111 | this.setNonEmptyMetadataProp(ffmpeg, 'artist', artist)
112 | this.setNonEmptyMetadataProp(ffmpeg, 'album', album)
113 | this.setNonEmptyMetadataProp(ffmpeg, 'comment', videoUrl)
114 |
115 | // Save and set art.
116 | const imgFile = `${this.audioFolder}/${videoId}.jpg`
117 | if (imgUrl) {
118 | try {
119 | spinner.start('Download art')
120 | await this.writeFile({
121 | file: imgFile,
122 | stream: download(imgUrl, { retry: 3 })
123 | })
124 | spinner.succeed('Art downloaded')
125 | spinner.start('Set art metadata')
126 | ffmpeg
127 | .addInput(imgFile)
128 | .outputOptions('-map', '0:0')
129 | .outputOptions('-map', '1:0')
130 | .outputOptions('-codec', 'copy')
131 | .outputOptions('-id3v2_version', '3')
132 | .save(tmpFile)
133 | } catch (error) {
134 | const errMessage = 'Error setting art metadata'
135 | spinner.fail(errMessage, error)
136 | reject(errMessage)
137 | }
138 | }
139 |
140 | ffmpeg.on('end', () => {
141 | try {
142 | fs.unlinkSync(file)
143 | fs.unlinkSync(imgFile)
144 | fs.renameSync(tmpFile, file)
145 | spinner.succeed('Art saved as metadata')
146 | resolve(ffmpeg)
147 | } catch (error) {
148 | const errMessage = 'Error removing temp files'
149 | spinner.fail(errMessage, error)
150 | reject(errMessage)
151 | }
152 | })
153 | })
154 | }
155 |
156 | stream (id, useCache) {
157 | if (useCache) {
158 | const cached = cache[id]
159 | if (cached) return cached
160 | }
161 |
162 | const video = ytdl(id)
163 | const ffmpeg = new Ffmpeg(video)
164 | const stream = through2()
165 |
166 | try {
167 | const ffmpegObj = ffmpeg.format('mp3').on('end', () => {
168 | cache[id] = null
169 | ffmpeg.kill()
170 | })
171 | ffmpegObj.pipe(stream, { end: true })
172 |
173 | cache[id] = stream
174 | return stream
175 | } catch (e) {
176 | throw e
177 | }
178 | }
179 |
180 | writeFile ({ file, stream }) {
181 | return new Promise((resolve, reject) => {
182 | const fileWriter = fs.createWriteStream(file)
183 | fileWriter.on('finish', () => {
184 | fileWriter.end()
185 | resolve()
186 | })
187 |
188 | fileWriter.on('error', error => {
189 | fileWriter.end()
190 | reject(error)
191 | })
192 | stream.pipe(fileWriter)
193 | })
194 | }
195 |
196 | async download ({ id, file, useCache, addMetadata, onMetadata }, callback) {
197 | // With metadata.
198 | if (addMetadata) {
199 | this.downloadWithMetadata({ id, file, useCache, addMetadata, onMetadata }, callback)
200 | return
201 | }
202 |
203 | // Without metadata.
204 | file =
205 | file || id.match(/^http.*/)
206 | ? `${this.audioFolder}/youtube-audio.mp3`
207 | : `${this.audioFolder}/${id}.mp3`
208 | spinner.start('Save audio')
209 | try {
210 | await this.writeFile({
211 | file,
212 | stream: await this.stream(id, useCache, addMetadata)
213 | })
214 | spinner.succeed('Audio saved')
215 | console.log(` ${gray(file)}`)
216 | callback(null, { id, file })
217 | } catch (error) {
218 | spinner.fail('Error saving audio', error.toString())
219 | callback(error)
220 | }
221 | }
222 |
223 | async downloadWithMetadata ({ id, file, useCache, addMetadata, onMetadata }, callback) {
224 | let metadata
225 | let filename
226 | try {
227 | metadata = await this.getMetadata(id)
228 | if (typeof onMetadata === 'function') {
229 | onMetadata({ id, ...metadata })
230 | }
231 | } catch (error) {
232 | callback(error)
233 | return
234 | }
235 |
236 | try {
237 | const { videoId, title } = metadata
238 | filename = sanitize(title || videoId)
239 | file = file || `${this.audioFolder}/${filename}.mp3`
240 |
241 | spinner.start('Save audio')
242 | await this.writeFile({
243 | file,
244 | stream: await this.stream(id, useCache, addMetadata)
245 | })
246 | spinner.succeed('Audio saved')
247 | } catch (error) {
248 | spinner.fail('Error saving audio', error.toString())
249 | callback(error)
250 | return
251 | }
252 |
253 | try {
254 | console.log(` ${gray(file)}`)
255 | await this.setMetadata({ file, id, metadata })
256 | callback(null, { id, file, filename })
257 | } catch (error) {
258 | callback(error)
259 | return
260 | }
261 | }
262 |
263 | search ({ query, page }, callback) {
264 | if (!this.ytNode) {
265 | console.log(red('YouTube KEY required and not set'))
266 | callback()
267 | return
268 | }
269 |
270 | if (page) {
271 | this.ytNode.addParam('pageToken', page)
272 | }
273 |
274 | this.ytNode.search(query, this.pageSize, callback)
275 | }
276 |
277 | get (id, callback) {
278 | if (!this.ytNode) {
279 | const error = 'YouTube KEY required and not set'
280 | console.log(red(error))
281 | callback(error)
282 | return
283 | }
284 | this.ytNode.getById(id, callback)
285 | }
286 | }
287 |
288 | module.exports = new YouTube()
289 |
--------------------------------------------------------------------------------