spottydl
Advanced tools
Comparing version 0.1.1 to 0.1.2
@@ -6,3 +6,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.downloadAlbum = exports.downloadTrack = void 0; | ||
exports.retryDownload = exports.downloadAlbum = exports.downloadTrack = void 0; | ||
const index_1 = require("./index"); | ||
@@ -14,2 +14,79 @@ const node_id3_1 = __importDefault(require("node-id3")); | ||
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 filename = `${oPath}${res.name}.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 filename = `${oPath}${res.name}.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 | ||
/** | ||
@@ -19,3 +96,3 @@ * Download the Spotify Track, need a <Track> type for first param, the second param is optional | ||
* @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 | ||
* @returns {Results[]} <Results[]> if successful, `string` if failed | ||
*/ | ||
@@ -25,4 +102,4 @@ const downloadTrack = async (obj, outputPath = './') => { | ||
// Check type and check if file path exists... | ||
if ((0, index_1.checkType)(obj) != "Track") { | ||
throw Error("obj passed is not of type <Track>"); | ||
if ((0, index_1.checkType)(obj) != 'Track') { | ||
throw Error('obj passed is not of type <Track>'); | ||
} | ||
@@ -37,25 +114,20 @@ let albCover = await axios_1.default.get(obj.albumCoverURL, { responseType: 'arraybuffer' }); | ||
image: { | ||
imageBuffer: Buffer.from(albCover.data, "utf-8") | ||
imageBuffer: Buffer.from(albCover.data, 'utf-8') | ||
} | ||
}; | ||
let filename = `${(0, index_1.checkPath)(outputPath)}${obj.title}.mp3`; | ||
let stream = (0, ytdl_core_1.default)(obj.id, { quality: 'highestaudio', filter: 'audioonly' }); | ||
return await new Promise((resolve, reject) => { | ||
(0, fluent_ffmpeg_1.default)(stream) | ||
.audioBitrate(128) | ||
.save(filename) | ||
.on('error', (err) => { | ||
console.error(`Failed to write file (${filename}): ${err}`); | ||
reject({ status: false, filename: filename, id: obj.id }); | ||
}) | ||
.on('end', () => { | ||
let tagStatus = node_id3_1.default.update(tags, filename); | ||
if (tagStatus) { | ||
resolve({ status: true, filename: filename, id: obj.id }); | ||
} | ||
else { | ||
reject({ status: false, filename: filename, id: obj.id }); | ||
} | ||
}); | ||
}); | ||
// 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 }]; | ||
} | ||
} | ||
@@ -70,13 +142,13 @@ catch (err) { | ||
* function will return an array of <Results> | ||
* @param {Track} obj An object of type <Album>, contains Album details and info | ||
* @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 | ||
* @returns {Results} <Results[]> if successful, `string` if failed | ||
* @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 = './') => { | ||
const downloadAlbum = async (obj, outputPath = './', sync = true) => { | ||
try { | ||
if ((0, index_1.checkType)(obj) != "Album") { | ||
throw Error("obj passed is not of type <Album>"); | ||
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 Results = []; // Use later inside the main loop, then we return this | ||
let tags = { | ||
@@ -87,38 +159,68 @@ artist: obj.artist, | ||
image: { | ||
imageBuffer: Buffer.from(albCover.data, "utf-8") | ||
imageBuffer: Buffer.from(albCover.data, 'utf-8') | ||
} | ||
}; | ||
let oPath = (0, index_1.checkPath)(outputPath); | ||
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 filename = `${oPath}${res.name}.mp3`; | ||
let stream = (0, ytdl_core_1.default)(res.id, { quality: 'highestaudio', filter: 'audioonly' }); | ||
(0, fluent_ffmpeg_1.default)(stream) | ||
.audioBitrate(128) | ||
.save(filename) | ||
.on('error', (err) => { | ||
Results.push({ status: false, filename: filename, id: res.id }); | ||
console.error(`Failed to write file (${filename}): ${err}`); | ||
(0, fs_1.unlinkSync)(filename); | ||
// reject(err) | ||
}) | ||
.on('end', () => { | ||
i++; | ||
Results.push({ status: true, filename: filename, id: res.id }); | ||
tags.title = res.name; | ||
tags.trackNumber = res.trackNumber; | ||
let tagStatus = node_id3_1.default.update(tags, filename); | ||
console.log(tagStatus ? `Finished: ${filename}` : `Failed to add tags: ${filename}`); | ||
if (i == obj.tracks.length) { | ||
resolve(Results); | ||
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; | ||
/** | ||
* 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[]} obj 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) { | ||
return `Caught: ${err}`; | ||
console.error(`Caught: ${err}`); | ||
return false; | ||
} | ||
}; | ||
exports.downloadAlbum = downloadAlbum; | ||
exports.retryDownload = retryDownload; |
@@ -6,4 +6,5 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.checkPath = exports.checkType = exports.downloadTrack = exports.downloadAlbum = exports.getTrack = exports.getAlbum = void 0; | ||
exports.checkPath = exports.checkType = exports.retryDownload = exports.downloadTrack = exports.downloadAlbum = exports.getTrack = exports.getAlbum = void 0; | ||
const fs_1 = require("fs"); | ||
const util_1 = require("util"); | ||
const os_1 = __importDefault(require("os")); | ||
@@ -16,16 +17,20 @@ var Info_1 = require("./Info"); | ||
Object.defineProperty(exports, "downloadTrack", { enumerable: true, get: function () { return Download_1.downloadTrack; } }); | ||
Object.defineProperty(exports, "retryDownload", { enumerable: true, get: function () { return Download_1.retryDownload; } }); | ||
/** | ||
* Check the type of the object, can be of type <Track> or <Album> | ||
* @param {Track|Album} ob An object, can be type <Track> or <Album> | ||
* @returns {string} "Track" | "Album" | "None" | ||
* Check the type of the object, can be of type <Track>, <Album> or <Results[]> | ||
* @param {Track|Album|Results[]} ob An object, can be type <Track>, <Album> or <Results[]> | ||
* @returns {string} "Track" | "Album" | "Results[]" | "None" | ||
*/ | ||
const checkType = (ob) => { | ||
if ("title" in ob && "trackNumber" in ob) { | ||
return "Track"; | ||
if ('title' in ob && 'trackNumber' in ob) { | ||
return 'Track'; | ||
} | ||
else if ("name" in ob && "tracks" in ob) { | ||
return "Album"; | ||
else if ('name' in ob && 'tracks' in ob) { | ||
return 'Album'; | ||
} | ||
else if ('status' in ob[0] && 'filename' in ob[0] && (0, util_1.isArray)(ob) == true) { | ||
return 'Results[]'; | ||
} | ||
else { | ||
return "None"; | ||
return 'None'; | ||
} | ||
@@ -43,3 +48,3 @@ }; | ||
if (!(0, fs_1.existsSync)(c)) { | ||
throw Error("Filepath:( " + c + " ) doesn't exist, please specify absolute path"); | ||
throw Error('Filepath:( ' + c + " ) doesn't exist, please specify absolute path"); | ||
} | ||
@@ -46,0 +51,0 @@ else if (c.slice(-1) != '/') { |
@@ -16,3 +16,3 @@ "use strict"; | ||
*/ | ||
const getTrack = async (url = "") => { | ||
const getTrack = async (url = '') => { | ||
try { | ||
@@ -33,3 +33,3 @@ // Check if url is a track URL | ||
album: spData.album.name, | ||
id: "ID", | ||
id: 'ID', | ||
albumCoverURL: spData.album.images[0].url, | ||
@@ -39,3 +39,3 @@ trackNumber: spData.track_number | ||
await ytm.initialize(); | ||
let trk = await ytm.search(`${tags.title} - ${tags.artist}`, "SONG"); | ||
let trk = await ytm.search(`${tags.title} - ${tags.artist}`, 'SONG'); | ||
tags.id = trk[0].videoId; | ||
@@ -54,3 +54,3 @@ return tags; | ||
*/ | ||
const getAlbum = async (url = "") => { | ||
const getAlbum = async (url = '') => { | ||
try { | ||
@@ -74,5 +74,9 @@ // Check if url is a track URL | ||
await ytm.initialize(); | ||
let alb = await ytm.search(`${tags.artist} - ${tags.name}`, "ALBUM"); | ||
let alb = await ytm.search(`${tags.artist} - ${tags.name}`, 'ALBUM'); | ||
let albData = await ytm.getAlbum(alb[0].albumId); | ||
albData.songs.map((i, n) => tags.tracks.push({ name: i.name, id: i.videoId, trackNumber: n + 1 })); | ||
albData.songs.map((i, n) => tags.tracks.push({ | ||
name: spData.tracks.items[n].name, | ||
id: i.videoId, | ||
trackNumber: spData.tracks.items[n].track_number | ||
})); | ||
return tags; | ||
@@ -79,0 +83,0 @@ } |
{ | ||
"name": "spottydl", | ||
"version": "0.1.1", | ||
"version": "0.1.2", | ||
"description": "NodeJS Spotify Downloader without any API Keys or Authentication", | ||
@@ -11,2 +11,3 @@ "main": "dist/index.js", | ||
"watch": "tsc -w", | ||
"format": "prettier --config .prettierrc 'src/*.ts' --write", | ||
"docs": "typedoc src/index.ts" | ||
@@ -42,2 +43,3 @@ }, | ||
"@types/node": "^17.0.8", | ||
"prettier": "^2.5.1", | ||
"typedoc": "^0.22.10", | ||
@@ -44,0 +46,0 @@ "typescript": "^4.5.4" |
@@ -11,3 +11,4 @@ # Spottydl | ||
- [x] **Simple and easy to use**, contains only 4 usable methods 🤔 I do need some help optimizing some parts | ||
- [ ] Error checking, when downloading Tracks or Albums, like retrying the process when status failed... | ||
- [x] Error checking, when downloading Tracks or Albums, like retrying the process when status failed... | ||
- [ ] Downloading playlists, as of now Spotify Tracks/Albums are currently supported | ||
@@ -94,8 +95,22 @@ ## Installation | ||
/* Example Output | ||
{ | ||
status: true, | ||
filename: '~/somePath/Never Gonna Give You Up.mp3', | ||
id: 'lYBUbBu4W08' | ||
/* Example Output (Successful) | ||
[ | ||
{ status: 'Success', filename: '~/somePath/Never Gonna Give You Up.mp3' } | ||
] | ||
*/ | ||
/* Example Output (Failed) | ||
[ | ||
{ | ||
status: 'Failed (stream)', | ||
filename: ~/somePath/Never Gonna Give You Up.mp3, | ||
id: 'lYBUbBu4W08', // videoId from YT-Music | ||
tags: { | ||
title: 'Never Gonna Give You Up', | ||
artist: 'Rick Astley', | ||
year: '1987-11-12', | ||
... | ||
} | ||
} | ||
] | ||
*/ | ||
@@ -108,5 +123,5 @@ ``` | ||
(async() => { | ||
await SpottyDL.getAlbum("https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT") | ||
await SpottyDL.getAlbum("https://open.spotify.com/album/66MRfhZmuTuyGCO1dJZTRB") | ||
.then(async(results) => { | ||
let album = await SpottyDL.downloadAlbum(results) | ||
let album = await SpottyDL.downloadAlbum(results, "output/", false) | ||
console.log(album) | ||
@@ -116,10 +131,20 @@ }); | ||
/* Example Output | ||
/* Example Output (Successful) | ||
[ | ||
{ | ||
status: true, | ||
filename: "./'K.'.mp3", | ||
id: 'L4sbDxR22z4' | ||
{ status: 'Success', filename: 'output/Crush.mp3' }, | ||
{ status: 'Success', filename: 'output/Sesame Syrup.mp3' } | ||
] | ||
*/ | ||
/* Example Output (Failed) some tracks failed proceed to use `retryDownload()` method | ||
[ | ||
{ | ||
status: 'Failed (Stream)', | ||
filename: 'output/Crush.mp3', | ||
id: YT-Music id, | ||
tags: { | ||
// tags for the track... | ||
} | ||
}, | ||
{...} | ||
{ status: 'Success', filename: 'output/Sesame Syrup.mp3' } | ||
] | ||
@@ -129,2 +154,29 @@ */ | ||
#### Retrying a failed download (Album/Track) | ||
```JS | ||
(async() => { | ||
await SpottyDL.getAlbum("https://open.spotify.com/album/66MRfhZmuTuyGCO1dJZTRB") | ||
.then(async(results) => { | ||
let album = await SpottyDL.downloadAlbum(results, "output/", false) | ||
let res = await SpottyDL.retryDownload(album); | ||
console.log(res) // boolean or <Results[]> | ||
}); | ||
})(); | ||
// Using a while loop until all tracks have no errors (Experimental) | ||
(async() => { | ||
await SpottyDL.getAlbum("https://open.spotify.com/album/66MRfhZmuTuyGCO1dJZTRB") | ||
.then(async(results) => { | ||
let album = await SpottyDL.downloadAlbum(results, "output/", false) | ||
let res = await SpottyDL.retryDownload(album); | ||
while(res != true) { | ||
res = await SpottyDL.retryDownload(res); | ||
console.log(res) // boolean or <Results[]> | ||
} | ||
}); | ||
})(); | ||
``` | ||
## Notes | ||
@@ -131,0 +183,0 @@ |
@@ -6,12 +6,20 @@ import { Album, Track, Results } from './index'; | ||
* @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 | ||
* @returns {Results[]} <Results[]> if successful, `string` if failed | ||
*/ | ||
export declare const downloadTrack: (obj: Track, outputPath?: string) => Promise<Results | string>; | ||
export declare const downloadTrack: (obj: Track, outputPath?: string) => Promise<Results[] | string>; | ||
/** | ||
* Download the Spotify Album, need a <Album> type for first param, the second param is optional, | ||
* function will return an array of <Results> | ||
* @param {Track} obj An object of type <Album>, contains Album details and info | ||
* @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 | ||
* @returns {Results} <Results[]> if successful, `string` if failed | ||
* @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 | ||
*/ | ||
export declare const downloadAlbum: (obj: Album, outputPath?: string) => Promise<Results[] | string>; | ||
export declare const downloadAlbum: (obj: Album, outputPath?: string, sync?: boolean) => Promise<Results[] | string>; | ||
/** | ||
* 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[]} obj 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. | ||
*/ | ||
export declare const retryDownload: (Info: Results[]) => Promise<Results[] | boolean>; |
export { getAlbum, getTrack } from './Info'; | ||
export { downloadAlbum, downloadTrack } from './Download'; | ||
export { downloadAlbum, downloadTrack, retryDownload } from './Download'; | ||
export declare type Track = { | ||
@@ -19,13 +19,14 @@ title: string; | ||
}; | ||
export declare type Results = { | ||
status: boolean; | ||
export interface Results { | ||
status: 'Success' | 'Failed (stream)' | 'Failed (tags)'; | ||
filename: string; | ||
id: string; | ||
}; | ||
id?: string; | ||
tags?: object; | ||
} | ||
/** | ||
* Check the type of the object, can be of type <Track> or <Album> | ||
* @param {Track|Album} ob An object, can be type <Track> or <Album> | ||
* @returns {string} "Track" | "Album" | "None" | ||
* Check the type of the object, can be of type <Track>, <Album> or <Results[]> | ||
* @param {Track|Album|Results[]} ob An object, can be type <Track>, <Album> or <Results[]> | ||
* @returns {string} "Track" | "Album" | "Results[]" | "None" | ||
*/ | ||
export declare const checkType: (ob: Track | Album) => "Track" | "Album" | "None"; | ||
export declare const checkType: (ob: Track | Album | Results[]) => 'Track' | 'Album' | 'Results[]' | 'None'; | ||
/** | ||
@@ -32,0 +33,0 @@ * Check the path if it exists, if not then we throw an error |
25353
428
192
5