spike-player
Version:
Complete framework to facilitate music commands using discord.js v13 & v14
410 lines (409 loc) • 18.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Utils = void 0;
const __1 = require("..");
const discord_js_1 = require("discord.js");
const ytsr_1 = __importDefault(require("ytsr"));
const fetch = require("isomorphic-unfetch");
const { getData, getPreview, getTracks, getDetails } = require("spotify-url-info")(fetch);
const soundcloud_scraper_1 = require("soundcloud-scraper");
const AppleUtils_1 = require("./AppleUtils");
const youtubei_1 = require("youtubei");
let YouTube = new youtubei_1.Client();
class Utils {
/**
*
*/
constructor() { }
/**
* Get ID from YouTube link
* @param {string} url
* @returns {?string}
*/
static parseVideo(url) {
const match = url.match(this.regexList.YouTubeVideoID);
return match ? match[7] : null;
}
/**
* Get timecode from YouTube link
* @param {string} url
* @returns {?string}
*/
static parseVideoTimecode(url) {
const match = url.match(this.regexList.YouTubeVideo);
return match ? match[10] : null;
}
/**
* Get ID from Playlist link
* @param {string} url
* @returns {?string}
*/
static parsePlaylist(url) {
const match = url.match(this.regexList.YouTubePlaylistID);
return match ? match[1] : null;
}
/**
* Search for Songs
* @param {string} Search
* @param {PlayOptions} [SOptions=DefaultPlayOptions]
* @param {Queue} Queue
* @param {number} [Limit=1]
* @return {Promise<Song[]>}
*/
static async search(Search, SOptions = __1.DefaultPlayOptions, Queue, Limit = 1) {
SOptions = Object.assign({}, __1.DefaultPlayOptions, SOptions);
let Filters;
try {
// Default Options - Type: Video
let FiltersTypes = await ytsr_1.default.getFilters(Search);
Filters = FiltersTypes.get("Type").get("Video");
// Custom Options - Upload date: null
if (SOptions?.uploadDate !== null)
Filters =
Array.from((await ytsr_1.default.getFilters(Filters.url)).get("Upload date"), ([name, value]) => ({ name, url: value.url })).find((o) => o.name.toLowerCase().includes(SOptions?.uploadDate)) ?? Filters;
// Custom Options - Duration: null
if (SOptions?.duration !== null)
Filters =
Array.from((await ytsr_1.default.getFilters(Filters.url)).get("Duration"), ([name, value]) => ({ name, url: value.url })).find((o) => o.name.toLowerCase().startsWith(SOptions?.duration)) ?? Filters;
// Custom Options - Sort by: relevance
if (SOptions?.sortBy !== null && SOptions?.sortBy !== "relevance")
Filters =
Array.from((await ytsr_1.default.getFilters(Filters.url)).get("Sort by"), ([name, value]) => ({ name, url: value.url })).find((o) => o.name.toLowerCase().includes(SOptions?.sortBy)) ?? Filters;
let Result = await (0, ytsr_1.default)(Filters.url, {
limit: Limit
});
let items = Result.items;
let songs = items
.map((item) => {
if (item?.type?.toLowerCase() !== "video")
return null;
return new __1.Song({
name: item.title,
url: item.url,
duration: item.duration,
author: item.author.name,
isLive: item.isLive,
thumbnail: item.bestThumbnail.url
}, Queue, SOptions.requestedBy);
})
.filter((I) => I);
return songs;
}
catch (e) {
throw __1.DMPErrors.SEARCH_NULL;
}
}
/**
* Search for Song via link
* @param {string} Search
* @param {PlayOptions} SOptions
* @param {Queue} Queue
* @return {Promise<Song>}
*/
static async link(Search, SOptions = __1.DefaultPlayOptions, Queue) {
let SpotifyLink = this.regexList.Spotify.test(Search);
let YouTubeLink = this.regexList.YouTubeVideo.test(Search);
let AppleLink = this.regexList.Apple.test(Search);
// console.log("SpotifyLink", SpotifyLink);
// console.log("YouTubeLink", YouTubeLink);
// console.log("AppleLink", AppleLink);
if (AppleLink) {
try {
let AppleResult = await (0, AppleUtils_1.getSong)(Search);
let SearchResult = await this.search(`${AppleResult.artist} - ${AppleResult.title}`, SOptions, Queue);
return SearchResult[0];
}
catch (e) {
throw __1.DMPErrors.INVALID_APPLE;
}
}
else if (SpotifyLink) {
try {
let SpotifyResult = await getPreview(Search);
let SearchResult = await this.search(`${SpotifyResult.artist} - ${SpotifyResult.title}`, SOptions, Queue);
return SearchResult[0];
}
catch (e) {
throw __1.DMPErrors.INVALID_SPOTIFY;
}
}
else if (YouTubeLink) {
let VideoID = this.parseVideo(Search);
if (!VideoID)
throw __1.DMPErrors.SEARCH_NULL;
YouTube = new youtubei_1.Client({
requestOptions: {
localAddress: SOptions.localAddress
}
});
// console.log("VideoID: ", VideoID);
let VideoResult = (await YouTube.getVideo(VideoID));
// console.log("VideoResult: ", VideoResult);
if (!VideoResult)
throw __1.DMPErrors.SEARCH_NULL;
let VideoTimecode = this.parseVideoTimecode(Search);
return new __1.Song({
name: VideoResult.title,
url: Search,
duration: this.msToTime((VideoResult.duration ?? 0) * 1000),
author: VideoResult.channel.name,
isLive: VideoResult.isLiveContent,
thumbnail: VideoResult.thumbnails.best,
seekTime: SOptions.timecode && VideoTimecode
? Number(VideoTimecode) * 1000
: null
}, Queue, SOptions.requestedBy);
}
else
return null;
}
/**
* Gets the best result of a Search
* @param {Song|string} Search
* @param {PlayOptions} SOptions
* @param {Queue} Queue
* @return {Promise<Song>}
*/
static async best(Search, SOptions = __1.DefaultPlayOptions, Queue) {
let _Song;
//console.log("Search", Search);
if (Search instanceof __1.Song)
return Search;
_Song = await this.link(Search, SOptions, Queue).catch((error) => {
if (!(error instanceof TypeError)) {
throw __1.DMPErrors.UNKNOWN; //Ignore typeError
}
});
if (!_Song)
_Song = (await this.search(Search, SOptions, Queue))[0];
return _Song;
}
/**
* Search for Playlist
* @param {string} Search
* @param {PlaylistOptions} SOptions
* @param {Queue} Queue
* @return {Promise<Playlist>}
*/
static async playlist(Search, SOptions = __1.DefaultPlaylistOptions, Queue) {
if (Search instanceof __1.Playlist)
return Search;
let Limit = SOptions.maxSongs ?? -1;
let SpotifyPlaylistLink = this.regexList.SpotifyPlaylist.test(Search);
let YouTubePlaylistLink = this.regexList.YouTubePlaylist.test(Search);
let ApplePlaylistLink = this.regexList.ApplePlaylist.test(Search);
let SoundCloudPlaylistLink = this.regexList.SoundCloudPlaylist.test(Search);
if (ApplePlaylistLink) {
let AppleResultData = await (0, AppleUtils_1.getPlaylist)(Search).catch(() => null);
if (!AppleResultData)
throw __1.DMPErrors.INVALID_PLAYLIST;
let AppleResult = {
name: AppleResultData.name,
author: AppleResultData.author,
url: Search,
songs: [],
type: AppleResultData.type
};
AppleResult.songs = (await Promise.all(AppleResultData.tracks.map(async (track, index) => {
if (Limit !== -1 && index >= Limit)
return null;
const Result = await this.search(`${track.artist} - ${track.title}`, SOptions, Queue).catch(() => null);
if (Result && Result[0]) {
Result[0].data = SOptions.data;
return Result[0];
}
else
return null;
}))).filter((V) => V !== null);
if (AppleResult.songs.length === 0)
throw __1.DMPErrors.INVALID_PLAYLIST;
if (SOptions.shuffle)
AppleResult.songs = this.shuffle(AppleResult.songs);
return new __1.Playlist(AppleResult, Queue, SOptions.requestedBy);
}
else if (SoundCloudPlaylistLink) {
let SoundCloudResultInfo = await new soundcloud_scraper_1.Client()
.getSongInfo(Search)
.catch(() => null);
let SoundCloudResultData = await new soundcloud_scraper_1.Client()
.getPlaylist(Search)
.catch(() => null);
if (!SoundCloudResultData)
throw __1.DMPErrors.INVALID_PLAYLIST;
let SoundCloudResult = {
name: SoundCloudResultData.title,
author: SoundCloudResultData.author.name,
url: Search,
songs: [],
type: "playlist"
};
SoundCloudResult.songs = (await Promise.all((SoundCloudResultData.tracks ?? []).map(async (track, index) => {
if (Limit !== -1 && index >= Limit)
return null;
const Result = await this.search(`${track.title}`, SOptions, Queue).catch(() => null);
if (Result) {
Result[0].data = SOptions.data;
return Result[0];
}
else
return null;
}))).filter((V) => V !== null);
if (SoundCloudResult.songs.length === 0)
throw __1.DMPErrors.INVALID_PLAYLIST;
if (SOptions.shuffle)
SoundCloudResult.songs = this.shuffle(SoundCloudResult.songs);
return new __1.Playlist(SoundCloudResult, Queue, SOptions.requestedBy);
}
else if (SpotifyPlaylistLink) {
let SpotifyResultData = await getData(Search).catch(() => null);
if (!SpotifyResultData ||
!["playlist", "album"].includes(SpotifyResultData.type))
throw __1.DMPErrors.INVALID_PLAYLIST;
let SpotifyResult = {
name: SpotifyResultData.name,
author: SpotifyResultData.type === "playlist"
? SpotifyResultData.owner.display_name
: SpotifyResultData.artists[0].name,
url: Search,
songs: [],
type: SpotifyResultData.type
};
SpotifyResult.songs = (await Promise.all((SpotifyResultData.tracks?.items ?? []).map(async (track, index) => {
if (Limit !== -1 && index >= Limit)
return null;
if (SpotifyResult.type === "playlist")
track = track.track;
const Result = await this.search(`${track.artists[0].name} - ${track.name}`, SOptions, Queue).catch(() => null);
if (Result) {
Result[0].data = SOptions.data;
return Result[0];
}
else
return null;
}))).filter((V) => V !== null);
if (SpotifyResult.songs.length === 0)
throw __1.DMPErrors.INVALID_PLAYLIST;
if (SOptions.shuffle)
SpotifyResult.songs = this.shuffle(SpotifyResult.songs);
return new __1.Playlist(SpotifyResult, Queue, SOptions.requestedBy);
}
else if (YouTubePlaylistLink) {
let PlaylistID = this.parsePlaylist(Search);
if (!PlaylistID)
throw __1.DMPErrors.INVALID_PLAYLIST;
YouTube = new youtubei_1.Client({
requestOptions: {
localAddress: SOptions.localAddress
}
});
let YouTubeResultData = await YouTube.getPlaylist(PlaylistID);
if (!YouTubeResultData ||
Object.keys(YouTubeResultData).length === 0)
throw __1.DMPErrors.INVALID_PLAYLIST;
let YouTubeResult = {
name: YouTubeResultData.title,
author: YouTubeResultData instanceof youtubei_1.Playlist
? YouTubeResultData.channel?.name ?? "YouTube Mix"
: "YouTube Mix",
url: Search,
songs: [],
type: "playlist"
};
if (YouTubeResultData instanceof youtubei_1.Playlist &&
YouTubeResultData.videoCount > 100 &&
(Limit === -1 || Limit > 100))
await YouTubeResultData.next(Math.floor((Limit === -1 || Limit > YouTubeResultData.videoCount
? YouTubeResultData.videoCount
: Limit - 1) / 100));
YouTubeResult.songs = YouTubeResultData.videos
.map((video, index) => {
if (Limit !== -1 && index >= Limit)
return null;
let song = new __1.Song({
name: video.title,
url: `https://youtube.com/watch?v=${video.id}`,
duration: this.msToTime((video.duration ?? 0) * 1000),
author: video.channel.name,
isLive: video.isLive,
thumbnail: video.thumbnails.best
}, Queue, SOptions.requestedBy);
song.data = SOptions.data;
return song;
})
.filter((V) => V !== null);
if (YouTubeResult.songs.length === 0)
throw __1.DMPErrors.INVALID_PLAYLIST;
if (SOptions.shuffle)
YouTubeResult.songs = this.shuffle(YouTubeResult.songs);
return new __1.Playlist(YouTubeResult, Queue, SOptions.requestedBy);
}
throw __1.DMPErrors.INVALID_PLAYLIST;
}
/**
* Shuffles an array
* @param {any[]} array
* @returns {any[]}
*/
static shuffle(array) {
if (!Array.isArray(array))
return [];
const clone = [...array];
const shuffled = [];
while (clone.length > 0)
shuffled.push(clone.splice(Math.floor(Math.random() * clone.length), 1)[0]);
return shuffled;
}
/**
* Converts milliseconds to duration (HH:MM:SS)
* @returns {string}
*/
static msToTime(duration) {
const seconds = Math.floor((duration / 1000) % 60);
const minutes = Math.floor((duration / 60000) % 60);
const hours = Math.floor(duration / 3600000);
const secondsPad = `${seconds}`.padStart(2, "0");
const minutesPad = `${minutes}`.padStart(2, "0");
const hoursPad = `${hours}`.padStart(2, "0");
return `${hours ? `${hoursPad}:` : ""}${minutesPad}:${secondsPad}`;
}
/**
* Converts duration (HH:MM:SS) to milliseconds
* @returns {number}
*/
static timeToMs(duration) {
return (duration
.split(":")
.reduceRight((prev, curr, i, arr) => prev + parseInt(curr) * 60 ** (arr.length - 1 - i), 0) * 1000);
}
static isVoiceChannel(Channel) {
let type = Channel.type;
if (typeof type === 'string')
return ['GUILD_VOICE', 'GUILD_STAGE_VOICE'].includes(type);
else
return [discord_js_1.ChannelType.GuildVoice, discord_js_1.ChannelType.GuildStageVoice].includes(type);
}
static isStageVoiceChannel(Channel) {
let type = Channel.type;
if (typeof type === 'string')
return type === 'GUILD_STAGE_VOICE';
else
return type === discord_js_1.ChannelType.GuildStageVoice;
}
}
exports.Utils = Utils;
Utils.regexList = {
YouTubeVideo: /^((?:https?:)\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))((?!channel)(?!user)\/(?:[\w\-]+\?v=|embed\/|v\/)?)((?!channel)(?!user)[\w\-]+)(((.*(\?|\&)t=(\d+))(\D?|\S+?))|\D?|\S+?)$/,
YouTubeVideoID: /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/,
YouTubePlaylist: /^((?:https?:)\/\/)?((?:www|m)\.)?((?:youtube\.com)).*(youtu.be\/|list=)([^#&?]*).*/,
YouTubePlaylistID: /[&?]list=([^&]+)/,
Spotify: /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-)+)(?:(?=\?)(?:[?&]foo=(\d*)(?=[&#]|$)|(?![?&]foo=)[^#])+)?(?=#|$)/,
SpotifyPlaylist: /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:(album|playlist)\/|\?uri=spotify:playlist:)((\w|-)+)(?:(?=\?)(?:[?&]foo=(\d*)(?=[&#]|$)|(?![?&]foo=)[^#])+)?(?=#|$)/,
Apple: /https?:\/\/music\.apple\.com\/.+?\/.+?\/(.+?)\//,
ApplePlaylist: /https?:\/\/music\.apple\.com\/.+?\/.+?\/(.+?)\//,
SoundCloudPlaylist: /https?:\/\/soundcloud\.com\/.*/,
SoundCloudUrl: /^https?:\/\/(www\.|m\.)?soundcloud\.com\/[a-z0-9](?!.*?(-|_){2})[\w-]{1,23}[a-z0-9](?:\/.+)?$/
};
Utils.audioTypes = [".mp3", ".wav", ".mp4", ".webm", ".mov", ".m4a"];