UNPKG

spottydl

Version:

NodeJS Spotify Downloader without any API Keys or Authentication

275 lines (274 loc) 11.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.retryDownload = exports.downloadPlaylist = exports.downloadAlbum = exports.downloadTrack = void 0; const index_1 = require("./index"); const node_id3_1 = __importDefault(require("node-id3")); const ytdl_core_1 = __importDefault(require("ytdl-core")); const fluent_ffmpeg_1 = __importDefault(require("fluent-ffmpeg")); const axios_1 = __importDefault(require("axios")); const fs_1 = require("fs"); // Private Methods const dl_track = async (id, filename) => { return await new Promise((resolve, reject) => { (0, fluent_ffmpeg_1.default)((0, ytdl_core_1.default)(id, { quality: 'highestaudio', filter: 'audioonly' })) .audioBitrate(128) .save(filename) .on('error', (err) => { console.error(`Failed to write file (${filename}): ${err}`); (0, fs_1.unlinkSync)(filename); resolve(false); }) .on('end', () => { resolve(true); }); }); }; const dl_album_normal = async (obj, oPath, tags) => { let Results = []; for await (let res of obj.tracks) { let sanitizedTitle = res.title.replace(/[/\\]/g, ' '); let filename = `${oPath}${sanitizedTitle}.mp3`; let dlt = await dl_track(res.id, filename); if (dlt) { let tagStatus = node_id3_1.default.update(tags, filename); if (tagStatus) { console.log(`Finished: ${filename}`); Results.push({ status: 'Success', filename: filename }); } else { console.log(`Failed: ${filename} (tags)`); Results.push({ status: 'Failed (tags)', filename: filename, tags: tags }); } } else { console.log(`Failed: ${filename} (stream)`); Results.push({ status: 'Failed (stream)', filename: filename, id: res.id, tags: tags }); } } return Results; }; const dl_album_fast = async (obj, oPath, tags) => { let Results = []; let i = 0; // Variable for specifying the index of the loop return await new Promise(async (resolve, reject) => { for await (let res of obj.tracks) { let sanitizedTitle = res.title.replace(/[/\\]/g, ' '); let filename = `${oPath}${sanitizedTitle}.mp3`; (0, fluent_ffmpeg_1.default)((0, ytdl_core_1.default)(res.id, { quality: 'highestaudio', filter: 'audioonly' })) .audioBitrate(128) .save(filename) .on('error', (err) => { tags.title = res.name; // Tags tags.trackNumber = res.trackNumber; Results.push({ status: 'Failed (stream)', filename: filename, id: res.id, tags: tags }); console.error(`Failed to write file (${filename}): ${err}`); (0, fs_1.unlinkSync)(filename); // reject(err) }) .on('end', () => { i++; tags.title = res.name; tags.trackNumber = res.trackNumber; let tagStatus = node_id3_1.default.update(tags, filename); if (tagStatus) { console.log(`Finished: ${filename}`); Results.push({ status: 'Success', filename: filename }); } else { console.log(`Failed to add tags: ${filename}`); Results.push({ status: 'Failed (tags)', filename: filename, id: res.id, tags: tags }); } if (i == obj.tracks.length) { resolve(Results); } }); } }); }; // END /** * Download the Spotify Track, need a <Track> type for first param, the second param is optional * @param {Track} obj An object of type <Track>, contains Track details and info * @param {string} outputPath - String type, (optional) if not specified the output will be on the current dir * @returns {Results[]} <Results[]> if successful, `string` if failed */ const downloadTrack = async (obj, outputPath = './') => { try { // Check type and check if file path exists... if ((0, index_1.checkType)(obj) != 'Track') { throw Error('obj passed is not of type <Track>'); } let albCover = await axios_1.default.get(obj.albumCoverURL, { responseType: 'arraybuffer' }); let tags = { title: obj.title, artist: obj.artist, album: obj.album, year: obj.year, trackNumber: obj.trackNumber, image: { imageBuffer: Buffer.from(albCover.data, 'utf-8') } }; let sanitizedTitle = obj.title.replace(/[/\\]/g, ' '); let filename = `${(0, index_1.checkPath)(outputPath)}${sanitizedTitle}.mp3`; // EXPERIMENTAL let dlt = await dl_track(obj.id, filename); if (dlt) { let tagStatus = node_id3_1.default.update(tags, filename); if (tagStatus) { return [{ status: 'Success', filename: filename }]; } else { return [{ status: 'Failed (tags)', filename: filename, tags: tags }]; } } else { return [{ status: 'Failed (stream)', filename: filename, id: obj.id, tags: tags }]; } } catch (err) { return `Caught: ${err}`; } }; exports.downloadTrack = downloadTrack; /** * Download the Spotify Album, need a <Album> type for first param, the second param is optional, * function will return an array of <Results> * @param {Album} obj An object of type <Album>, contains Album details and info * @param {string} outputPath - String type, (optional) if not specified the output will be on the current dir * @param {boolean} sync - Boolean type, (optional) can be `true` or `false`. Default (true) is safer/less errors, for slower bandwidths * @returns {Results[]} <Results[]> if successful, `string` if failed */ const downloadAlbum = async (obj, outputPath = './', sync = true) => { try { if ((0, index_1.checkType)(obj) != 'Album') { throw Error('obj passed is not of type <Album>'); } let albCover = await axios_1.default.get(obj.albumCoverURL, { responseType: 'arraybuffer' }); let tags = { artist: obj.artist, album: obj.name, year: obj.year, image: { imageBuffer: Buffer.from(albCover.data, 'utf-8') } }; let oPath = (0, index_1.checkPath)(outputPath); if (sync) { return await dl_album_normal(obj, oPath, tags); } else { return await dl_album_fast(obj, oPath, tags); } } catch (err) { return `Caught: ${err}`; } }; exports.downloadAlbum = downloadAlbum; /** * Download the Spotify Playlist, need a <Playlist> type for first param, the second param is optional, * function will return an array of <Results> * @param {Playlist} obj An object of type <Playlist>, contains Playlist details and info * @param {string} outputPath - String type, (optional) if not specified the output will be on the current dir * @returns {Results[]} <Results[]> if successful, `string` if failed */ const downloadPlaylist = async (obj, outputPath = './') => { try { let Results = []; if ((0, index_1.checkType)(obj) != 'Playlist') { throw Error('obj passed is not of type <Playlist>'); } let oPath = (0, index_1.checkPath)(outputPath); for await (let res of obj.tracks) { let sanitizedTitle = res.title.replace(/[/\\]/g, ' '); let filename = `${oPath}${sanitizedTitle}.mp3`; let dlt = await dl_track(res.id, filename); let albCover = await axios_1.default.get(res.albumCoverURL, { responseType: 'arraybuffer' }); let tags = { title: res.title, artist: res.artist, album: res.album, // year: 0, // Year tag doesn't exist when scraping trackNumber: res.trackNumber, image: { imageBuffer: Buffer.from(albCover.data, 'utf-8') } }; if (dlt) { let tagStatus = node_id3_1.default.update(tags, filename); if (tagStatus) { console.log(`Finished: ${filename}`); Results.push({ status: 'Success', filename: filename }); } else { console.log(`Failed: ${filename} (tags)`); Results.push({ status: 'Failed (tags)', filename: filename, tags: tags }); } } else { console.log(`Failed: ${filename} (stream)`); Results.push({ status: 'Failed (stream)', filename: filename, id: res.id, tags: tags }); } } return Results; } catch (err) { return `Caught: ${err}`; } }; exports.downloadPlaylist = downloadPlaylist; /** * Retries the download process if there are errors. Only use this after `downloadTrack()` or `downloadAlbum()` methods * checks for failed downloads then tries again, returns <Results[]> object array * @param {Results[]} Info An object of type <Results[]>, contains an array of results * @returns {Results[]} <Results[]> array if the download process is successful, `true` if there are no errors and `false` if an error happened. */ const retryDownload = async (Info) => { try { if ((0, index_1.checkType)(Info) != 'Results[]') { throw Error('obj passed is not of type <Results[]>'); } // Filter the results let failedStream = Info.filter((i) => i.status == 'Failed (stream)' || i.status == 'Failed (tags)'); if (failedStream.length == 0) { return true; } let Results = []; failedStream.map(async (i) => { if (i.status == 'Failed (stream)') { let dlt = await dl_track(i.id, i.filename); if (dlt) { let tagStatus = node_id3_1.default.update(i.tags, i.filename); if (tagStatus) { Results.push({ status: 'Success', filename: i.filename }); } else { Results.push({ status: 'Failed (tags)', filename: i.filename, tags: i.tags }); } } else { Results.push({ status: 'Failed (stream)', filename: i.filename, id: i.id, tags: i.tags }); } } else if (i.status == 'Failed (tags)') { let tagStatus = node_id3_1.default.update(i.tags, i.filename); if (tagStatus) { Results.push({ status: 'Success', filename: i.filename }); } else { Results.push({ status: 'Failed (tags)', filename: i.filename, tags: i.tags }); } } }); return Results; } catch (err) { console.error(`Caught: ${err}`); return false; } }; exports.retryDownload = retryDownload;