"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { Downloader: () => Downloader, FormatConverter: () => FormatConverter, SongTagsSearch: () => SongTagsSearch, YtdlMp3Error: () => YtdlMp3Error }); module.exports = __toCommonJS(src_exports); // src/Downloader.ts var import_os = __toESM(require("os"), 1); var import_path = __toESM(require("path"), 1); var import_ytdl_core = __toESM(require("@distube/ytdl-core"), 1); var import_node_id3 = __toESM(require("node-id3"), 1); // src/FormatConverter.ts var import_child_process = __toESM(require("child_process"), 1); var import_fs2 = __toESM(require("fs"), 1); var import_ffmpeg_static = __toESM(require("ffmpeg-static"), 1); // src/utils.ts var import_fs = __toESM(require("fs"), 1); var import_readline = __toESM(require("readline"), 1); function removeParenthesizedText(s) { const regex = /\s*([[(][^[\]()]*[\])])\s*/g; while (regex.test(s)) { s = s.replace(regex, ""); } return s; } function isDirectory(path2) { return import_fs.default.existsSync(path2) && import_fs.default.lstatSync(path2).isDirectory(); } async function userInput(prompt, defaultInput) { const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve, reject) => { rl.question(prompt, (response) => { rl.close(); if (response) { resolve(response); } else { reject(new YtdlMp3Error("Invalid response: " + response)); } }); rl.write(defaultInput ?? ""); }); } var YtdlMp3Error = class extends Error { constructor(message, options) { super(message, options); this.name = "YtdlMp3Error"; } }; // src/FormatConverter.ts var FormatConverter = class { ffmpegBinary; constructor() { if (!import_ffmpeg_static.default) { throw new YtdlMp3Error("Failed to resolve ffmpeg binary"); } this.ffmpegBinary = import_ffmpeg_static.default; } videoToAudio(videoData, outputFile) { if (import_fs2.default.existsSync(outputFile)) { throw new YtdlMp3Error(`Output file already exists: ${outputFile}`); } import_child_process.default.execSync(`${this.ffmpegBinary} -loglevel 24 -i pipe:0 -vn -sn -c:a mp3 -ab 192k ${outputFile}`, { input: videoData }); } }; // src/SongTagsSearch.ts var import_axios = __toESM(require("axios"), 1); var SongTagsSearch = class { searchTerm; url; constructor(videoDetails) { this.searchTerm = removeParenthesizedText(videoDetails.title); this.url = new URL("https://itunes.apple.com/search?"); this.url.searchParams.set("media", "music"); this.url.searchParams.set("term", this.searchTerm); } async search(verify = false) { console.log(`Attempting to query iTunes API with the following search term: ${this.searchTerm}`); const searchResults = await this.fetchResults(); const result = verify ? await this.getVerifiedResult(searchResults) : searchResults[0]; const artworkUrl = result.artworkUrl100.replace("100x100bb.jpg", "600x600bb.jpg"); const albumArt = await this.fetchAlbumArt(artworkUrl); return { artist: result.artistName, image: { description: "Album Art", imageBuffer: albumArt, mime: "image/png", type: { id: 3, name: "front cover" } }, title: result.trackName }; } async fetchAlbumArt(url) { return import_axios.default.get(url, { responseType: "arraybuffer" }).then((response) => Buffer.from(response.data, "binary")).catch(() => { throw new YtdlMp3Error("Failed to fetch album art from endpoint: " + url); }); } async fetchResults() { const response = await import_axios.default.get(this.url.href).catch((error) => { if (error.response?.status) { throw new YtdlMp3Error(`Call to iTunes API returned status code ${error.response.status}`); } throw new YtdlMp3Error("Call to iTunes API failed and did not return a status"); }); if (response.data.resultCount === 0) { throw new YtdlMp3Error("Call to iTunes API did not return any results"); } return response.data.results; } async getVerifiedResult(searchResults) { for (const result of searchResults) { console.log("The following tags were extracted from iTunes:"); console.log("Title: " + result.trackName); console.log("Artist: " + result.artistName); const validResponses = ["Y", "YES", "N", "NO"]; let userSelection = (await userInput("Please verify (Y/N): ")).toUpperCase(); while (!validResponses.includes(userSelection)) { console.error("Invalid selection, try again!"); userSelection = (await userInput("Please verify (Y/N): ")).toUpperCase(); } if (userSelection === "Y" || userSelection === "YES") { return result; } } throw new YtdlMp3Error("End of results"); } }; // src/Downloader.ts var Downloader = class _Downloader { static defaultDownloadsDir = import_path.default.join(import_os.default.homedir(), "Downloads"); getTags; outputDir; silentMode; verifyTags; constructor({ getTags, outputDir, silentMode, verifyTags }) { this.outputDir = outputDir ?? _Downloader.defaultDownloadsDir; this.getTags = Boolean(getTags); this.silentMode = Boolean(silentMode); this.verifyTags = Boolean(verifyTags); } async downloadSong(url) { if (!isDirectory(this.outputDir)) { throw new YtdlMp3Error(`Not a directory: ${this.outputDir}`); } const videoInfo = await import_ytdl_core.default.getInfo(url).catch((error) => { throw new YtdlMp3Error(`Failed to fetch info for video with URL: ${url}`, { cause: error }); }); const formatConverter = new FormatConverter(); const songTagsSearch = new SongTagsSearch(videoInfo.videoDetails); const outputFile = this.getOutputFile(videoInfo.videoDetails.title); const videoData = await this.downloadVideo(videoInfo).catch((error) => { throw new YtdlMp3Error("Failed to download video", { cause: error }); }); formatConverter.videoToAudio(videoData, outputFile); if (this.getTags) { const songTags = await songTagsSearch.search(this.verifyTags); import_node_id3.default.write(songTags, outputFile); } if (!this.silentMode) console.log(`Done! Output file: ${outputFile}`); return outputFile; } /** Returns the content from the video as a buffer */ async downloadVideo(videoInfo) { const buffers = []; const stream = import_ytdl_core.default.downloadFromInfo(videoInfo, { quality: "highestaudio" }); return new Promise((resolve, reject) => { stream.on("data", (chunk) => { buffers.push(chunk); }); stream.on("end", () => { resolve(Buffer.concat(buffers)); }); stream.on("error", (err) => { reject(err); }); }); } /** Returns the absolute path to the audio file to be downloaded */ getOutputFile(videoTitle) { const baseFileName = removeParenthesizedText(videoTitle).replace(/[^a-z0-9]/gi, "_").split("_").filter((element) => element).join("_").toLowerCase(); return import_path.default.join(this.outputDir, baseFileName + ".mp3"); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Downloader, FormatConverter, SongTagsSearch, YtdlMp3Error }); //# sourceMappingURL=index.cjs.map