UNPKG

@discord-player/extractor

Version:
1,461 lines (1,444 loc) 203 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/extractors/SoundCloudExtractor.ts import { BaseExtractor, Playlist, QueryType, Track, Util } from "discord-player"; import * as SoundCloud from "soundcloud.ts"; // src/internal/helper.ts import unfetch from "isomorphic-unfetch"; var UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.49"; var fetch = unfetch; function filterSoundCloudPreviews(tracks) { const filtered = tracks.filter((t) => { if (typeof t.policy === "string") return t.policy.toUpperCase() === "ALLOW"; return !(t.duration === 3e4 && t.full_duration > 3e4); }); const result = filtered.length > 0 ? filtered : tracks; return result; } __name(filterSoundCloudPreviews, "filterSoundCloudPreviews"); // src/extractors/SoundCloudExtractor.ts var _SoundCloudExtractor = class _SoundCloudExtractor extends BaseExtractor { constructor() { super(...arguments); __publicField(this, "internal", new SoundCloud.default({ clientId: this.options.clientId, oauthToken: this.options.oauthToken, proxy: this.options.proxy })); } async activate() { this.protocols = ["scsearch", "soundcloud"]; _SoundCloudExtractor.instance = this; } async deactivate() { this.protocols = []; _SoundCloudExtractor.instance = null; } async validate(query, type) { if (typeof query !== "string") return false; return [ QueryType.SOUNDCLOUD, QueryType.SOUNDCLOUD_PLAYLIST, QueryType.SOUNDCLOUD_SEARCH, QueryType.SOUNDCLOUD_TRACK, QueryType.AUTO, QueryType.AUTO_SEARCH ].some((r) => r === type); } async getRelatedTracks(track, history) { if (track.queryType === QueryType.SOUNDCLOUD_TRACK) { const data = await this.internal.tracks.relatedV2(track.url, 5); const unique = filterSoundCloudPreviews(data).filter( (t) => !history.tracks.some((h) => h.url === t.permalink_url) ); return this.createResponse( null, (unique.length > 0 ? unique : data).map((trackInfo) => { const newTrack = new Track(this.context.player, { title: trackInfo.title, url: trackInfo.permalink_url, duration: Util.buildTimeCode(Util.parseMS(trackInfo.duration)), description: trackInfo.description ?? "", thumbnail: trackInfo.artwork_url, views: trackInfo.playback_count, author: trackInfo.user.username, requestedBy: track.requestedBy, source: "soundcloud", engine: trackInfo, queryType: QueryType.SOUNDCLOUD_TRACK, metadata: trackInfo, requestMetadata: /* @__PURE__ */ __name(async () => { return trackInfo; }, "requestMetadata"), cleanTitle: trackInfo.title }); newTrack.extractor = this; return newTrack; }) ); } return this.createResponse(); } async handle(query, context) { if (context.protocol === "scsearch") context.type = QueryType.SOUNDCLOUD_SEARCH; switch (context.type) { case QueryType.SOUNDCLOUD_TRACK: { const trackInfo = await this.internal.tracks.getV2(query).catch(Util.noop); if (!trackInfo) return this.emptyResponse(); const track = new Track(this.context.player, { title: trackInfo.title, url: trackInfo.permalink_url, duration: Util.buildTimeCode(Util.parseMS(trackInfo.duration)), description: trackInfo.description ?? "", thumbnail: trackInfo.artwork_url, views: trackInfo.playback_count, author: trackInfo.user.username, requestedBy: context.requestedBy, source: "soundcloud", engine: trackInfo, queryType: context.type, metadata: trackInfo, requestMetadata: /* @__PURE__ */ __name(async () => { return trackInfo; }, "requestMetadata"), cleanTitle: trackInfo.title }); track.extractor = this; return { playlist: null, tracks: [track] }; } case QueryType.SOUNDCLOUD_PLAYLIST: { const data = await this.internal.playlists.getV2(query).catch(Util.noop); if (!data) return { playlist: null, tracks: [] }; const res = new Playlist(this.context.player, { title: data.title, description: data.description ?? "", thumbnail: data.artwork_url ?? data.tracks[0].artwork_url, type: "playlist", source: "soundcloud", author: { name: data.user.username, url: data.user.permalink_url }, tracks: [], id: `${data.id}`, url: data.permalink_url, rawPlaylist: data }); for (const song of data.tracks) { const track = new Track(this.context.player, { title: song.title, description: song.description ?? "", author: song.user.username, url: song.permalink_url, thumbnail: song.artwork_url, duration: Util.buildTimeCode(Util.parseMS(song.duration)), views: song.playback_count, requestedBy: context.requestedBy, playlist: res, source: "soundcloud", engine: song, queryType: context.type, metadata: song, requestMetadata: /* @__PURE__ */ __name(async () => { return song; }, "requestMetadata"), cleanTitle: song.title }); track.extractor = this; track.playlist = res; res.tracks.push(track); } return { playlist: res, tracks: res.tracks }; } default: { let tracks = await this.internal.tracks.searchV2({ q: query }).then((t) => t.collection).catch(Util.noop); if (!tracks) tracks = await this.internal.tracks.searchAlt(query).catch(Util.noop); if (!tracks || !tracks.length) return this.emptyResponse(); tracks = filterSoundCloudPreviews(tracks); const resolvedTracks = []; for (const trackInfo of tracks) { if (!trackInfo.streamable) continue; const track = new Track(this.context.player, { title: trackInfo.title, url: trackInfo.permalink_url, duration: Util.buildTimeCode(Util.parseMS(trackInfo.duration)), description: trackInfo.description ?? "", thumbnail: trackInfo.artwork_url, views: trackInfo.playback_count, author: trackInfo.user.username, requestedBy: context.requestedBy, source: "soundcloud", engine: trackInfo, queryType: "soundcloudTrack", metadata: trackInfo, requestMetadata: /* @__PURE__ */ __name(async () => { return trackInfo; }, "requestMetadata") }); track.extractor = this; resolvedTracks.push(track); } return { playlist: null, tracks: resolvedTracks }; } } } emptyResponse() { return { playlist: null, tracks: [] }; } async stream(info) { const url = await this.internal.util.streamLink(info.url).catch(Util.noop); if (!url) throw new Error("Could not extract stream from this track source"); return url; } async bridge(track, sourceExtractor) { if (sourceExtractor?.identifier === this.identifier) { return this.stream(track); } const query = sourceExtractor?.createBridgeQuery(track) ?? `${track.author} - ${track.title}`; const info = await this.handle(query, { requestedBy: track.requestedBy, type: QueryType.SOUNDCLOUD_SEARCH }); if (!info.tracks.length) return null; const result = await this.stream(info.tracks[0]); if (result) { track.bridgedTrack = info.tracks[0]; track.bridgedExtractor = this; } return result; } }; __name(_SoundCloudExtractor, "SoundCloudExtractor"); __publicField(_SoundCloudExtractor, "identifier", "com.discord-player.soundcloudextractor"); __publicField(_SoundCloudExtractor, "instance", null); var SoundCloudExtractor = _SoundCloudExtractor; // src/extractors/LyricsExtractor.ts function lyricsExtractor() { throw new Error( "Legacy lyrics extractor has been removed. Please use the new lyrics api from `player.lyrics` instead. It offers more accurate result and features like plain lyrics and synced lyrics." ); } __name(lyricsExtractor, "lyricsExtractor"); // src/extractors/VimeoExtractor.ts import { BaseExtractor as BaseExtractor2, QueryType as QueryType2, Track as Track2, Util as Util2 } from "discord-player"; // src/internal/Vimeo.ts import http from "http"; import https from "https"; var _Vimeo = class _Vimeo { constructor() { throw new Error( `The ${this.constructor.name} class may not be instantiated!` ); } /** * @typedef {Readable} Readable */ /** * Downloads from vimeo * @param {number} id Vimeo video id * @returns {Promise<Readable>} */ static download(id) { return new Promise(async (resolve) => { const info = await _Vimeo.getInfo(id); if (!info) return null; const downloader = info.stream.startsWith("https://") ? https : http; downloader.get(info.stream, (res) => { resolve(res); }); }); } /** * Returns video info * @param {number} id Video id */ static async getInfo(id) { if (!id) throw new Error("Invalid id"); const url = `https://player.vimeo.com/video/${id}`; try { const res = await fetch(url); const data = await res.text(); const json = JSON.parse( data.split("window.playerConfig =")[1].split(";")[0].trim() ); const obj = { id: json.video.id, duration: json.video.duration * 1e3, title: json.video.title, url: json.video.url, thumbnail: json.video.thumbs["1280"] || json.video.thumbs.base, stream: json.request.files.progressive[0].url, author: { id: json.video.owner.id, name: json.video.owner.name, url: json.video.owner.url, avatar: json.video.owner.img_2x || json.video.owner.img } }; return obj; } catch { return null; } } }; __name(_Vimeo, "Vimeo"); var Vimeo = _Vimeo; // src/extractors/VimeoExtractor.ts var _VimeoExtractor = class _VimeoExtractor extends BaseExtractor2 { async validate(query, type) { if (typeof query !== "string") return false; return [QueryType2.VIMEO].some((r) => r === type); } async getRelatedTracks(track) { return this.createResponse(); } async handle(query, context) { switch (context.type) { case QueryType2.VIMEO: { const trackInfo = await Vimeo.getInfo( query.split("/").filter((x) => !!x).pop() ).catch(Util2.noop); if (!trackInfo) return this.emptyResponse(); const track = new Track2(this.context.player, { title: trackInfo.title, url: trackInfo.url, duration: Util2.buildTimeCode(Util2.parseMS(trackInfo.duration || 0)), description: `${trackInfo.title} by ${trackInfo.author.name}`, thumbnail: trackInfo.thumbnail, views: 0, author: trackInfo.author.name, requestedBy: context.requestedBy, source: "arbitrary", engine: trackInfo.stream, queryType: context.type, metadata: trackInfo, async requestMetadata() { return trackInfo; } }); track.extractor = this; return { playlist: null, tracks: [track] }; } default: return this.emptyResponse(); } } emptyResponse() { return { playlist: null, tracks: [] }; } async stream(info) { const engine = info.raw.engine; if (engine) { return engine; } const track = await Vimeo.getInfo(info.url).catch(Util2.noop); if (!track || !track.stream) throw new Error("Could not extract stream from this source"); info.raw.engine = { streamURL: track.stream }; return track.stream; } }; __name(_VimeoExtractor, "VimeoExtractor"); __publicField(_VimeoExtractor, "identifier", "com.discord-player.vimeoextractor"); var VimeoExtractor = _VimeoExtractor; // src/extractors/ReverbnationExtractor.ts import { BaseExtractor as BaseExtractor3, QueryType as QueryType3, Track as Track3, Util as Util3 } from "discord-player"; import reverbnation from "reverbnation-scraper"; var _ReverbnationExtractor = class _ReverbnationExtractor extends BaseExtractor3 { async validate(query, type) { if (typeof query !== "string") return false; return [QueryType3.REVERBNATION].some( (r) => r === type ); } async getRelatedTracks(track) { return this.createResponse(); } async handle(query, context) { switch (context.type) { case QueryType3.REVERBNATION: { const trackInfo = await reverbnation.getInfo(query).catch(Util3.noop); if (!trackInfo) return this.emptyResponse(); const track = new Track3(this.context.player, { title: trackInfo.title, url: trackInfo.url, duration: Util3.buildTimeCode(Util3.parseMS(trackInfo.duration)), description: trackInfo.lyrics || `${trackInfo.title} by ${trackInfo.artist.name}`, thumbnail: trackInfo.thumbnail, views: 0, author: trackInfo.artist.name, requestedBy: context.requestedBy, source: "arbitrary", engine: trackInfo.streamURL, queryType: context.type, metadata: trackInfo, async requestMetadata() { return trackInfo; } }); track.extractor = this; return { playlist: null, tracks: [track] }; } default: return this.emptyResponse(); } } emptyResponse() { return { playlist: null, tracks: [] }; } async stream(info) { const engine = info.raw.engine; if (engine) { return engine; } const track = await reverbnation.getInfo(info.url).catch(Util3.noop); if (!track || !track.streamURL) throw new Error("Could not extract stream from this source"); info.raw.engine = { streamURL: track.streamURL }; return track.streamURL; } }; __name(_ReverbnationExtractor, "ReverbnationExtractor"); __publicField(_ReverbnationExtractor, "identifier", "com.discord-player.reverbnationextractor"); var ReverbnationExtractor = _ReverbnationExtractor; // src/extractors/AttachmentExtractor.ts import { BaseExtractor as BaseExtractor4, QueryType as QueryType4, Track as Track4, Util as Util4 } from "discord-player"; import { createReadStream, existsSync } from "fs"; // src/internal/downloader.ts import http2 from "http"; import https2 from "https"; function downloadStream(url, opts = {}) { return new Promise((resolve, reject) => { const lib = url.startsWith("http://") ? http2 : https2; lib.get(url, opts, (res) => resolve(res)).once("error", reject); }); } __name(downloadStream, "downloadStream"); // src/extractors/AttachmentExtractor.ts import * as fileType from "file-type"; import path from "path"; import { stat } from "fs/promises"; var ATTACHMENT_HEADER = ["audio/", "video/", "application/ogg"]; var _AttachmentExtractor = class _AttachmentExtractor extends BaseExtractor4 { constructor() { super(...arguments); // use lowest priority to avoid conflict with other extractors __publicField(this, "priority", 0); } async validate(query, type) { if (typeof query !== "string") return false; return [QueryType4.ARBITRARY, QueryType4.FILE].some( (r) => r === type ); } async getRelatedTracks(track) { return this.createResponse(); } async handle(query, context) { switch (context.type) { case QueryType4.ARBITRARY: { const data = await downloadStream( query, context.requestOptions ); if (!ATTACHMENT_HEADER.some( (r) => !!data.headers["content-type"]?.startsWith(r) )) return this.emptyResponse(); const trackInfo = { title: (query.split("/").filter((x) => x.length).pop() ?? "Attachment").split("?")[0].trim(), duration: 0, thumbnail: "https://upload.wikimedia.org/wikipedia/commons/2/2a/ITunes_12.2_logo.png", engine: query, // eslint-disable-next-line author: data.client?.servername || "Attachment", // eslint-disable-next-line description: data.client?.servername || "Attachment", url: data.url || query }; try { const mediaplex = __require("mediaplex"); const timeout = this.context.player.options.probeTimeout ?? 5e3; const { result, stream } = await Promise.race( [ mediaplex.probeStream(data), new Promise((_, r) => { setTimeout(() => r(new Error("Timeout")), timeout); }) ] ); if (result) { trackInfo.duration = result.duration * 1e3; const metadata = mediaplex.readMetadata(result); if (metadata.author) trackInfo.author = metadata.author; if (metadata.title) trackInfo.title = metadata.title; trackInfo.description = `${trackInfo.title} by ${trackInfo.author}`; } stream.destroy(); } catch { } const track = new Track4(this.context.player, { title: trackInfo.title, url: trackInfo.url, duration: Util4.buildTimeCode(Util4.parseMS(trackInfo.duration)), description: trackInfo.description, thumbnail: trackInfo.thumbnail, views: 0, author: trackInfo.author, requestedBy: context.requestedBy, source: "arbitrary", engine: trackInfo.url, queryType: context.type, metadata: trackInfo, async requestMetadata() { return trackInfo; } }); track.extractor = this; track.raw.isFile = false; return { playlist: null, tracks: [track] }; } case QueryType4.FILE: { if (!existsSync(query)) return this.emptyResponse(); const fstat = await stat(query); if (!fstat.isFile()) return this.emptyResponse(); const mime = await fileType.fromFile(query).catch(() => null); if (!mime || !ATTACHMENT_HEADER.some((r) => !!mime.mime.startsWith(r))) return this.emptyResponse(); const trackInfo = { title: path.basename(query) || "Attachment", duration: 0, thumbnail: "https://upload.wikimedia.org/wikipedia/commons/2/2a/ITunes_12.2_logo.png", engine: query, author: "Attachment", description: "Attachment", url: query }; try { const mediaplex = __require("mediaplex"); const timeout = this.context.player.options.probeTimeout ?? 5e3; const { result, stream } = await Promise.race( [ mediaplex.probeStream( createReadStream(query, { start: 0, end: 1024 * 1024 * 10 }) ), new Promise((_, r) => { setTimeout(() => r(new Error("Timeout")), timeout); }) ] ); if (result) { trackInfo.duration = result.duration * 1e3; const metadata = mediaplex.readMetadata(result); if (metadata.author) trackInfo.author = metadata.author; if (metadata.title) trackInfo.title = metadata.title; trackInfo.description = `${trackInfo.title} by ${trackInfo.author}`; } stream.destroy(); } catch { } const track = new Track4(this.context.player, { title: trackInfo.title, url: trackInfo.url, duration: Util4.buildTimeCode(Util4.parseMS(trackInfo.duration)), description: trackInfo.description, thumbnail: trackInfo.thumbnail, views: 0, author: trackInfo.author, requestedBy: context.requestedBy, source: "arbitrary", engine: trackInfo.url, queryType: context.type, metadata: trackInfo, async requestMetadata() { return trackInfo; } }); track.extractor = this; track.raw.isFile = true; return { playlist: null, tracks: [track] }; } default: return this.emptyResponse(); } } emptyResponse() { return { playlist: null, tracks: [] }; } async stream(info) { const engine = info.raw.engine; const isFile = info.raw.isFile; if (!engine) throw new Error("Could not find stream source"); if (!isFile) { return engine; } return createReadStream(engine); } }; __name(_AttachmentExtractor, "AttachmentExtractor"); __publicField(_AttachmentExtractor, "identifier", "com.discord-player.attachmentextractor"); var AttachmentExtractor = _AttachmentExtractor; // src/extractors/AppleMusicExtractor.ts import { Playlist as Playlist2, QueryType as QueryType5, Track as Track5, Util as Util5, BaseExtractor as BaseExtractor5 } from "discord-player"; // src/internal/index.ts var internal_exports = {}; __export(internal_exports, { AppleMusic: () => AppleMusic, SpotifyAPI: () => SpotifyAPI, Vimeo: () => Vimeo, downloadStream: () => downloadStream }); // src/internal/AppleMusic.ts import { QueryResolver } from "discord-player"; import { parse } from "node-html-parser"; function getHTML(link) { return fetch(link, { headers: { "User-Agent": UA } }).then((r) => r.text()).then( (txt) => parse(txt), () => null ); } __name(getHTML, "getHTML"); function makeImage({ height, url, width, ext = "jpg" }) { return url.replace("{w}", `${width}`).replace("{h}", `${height}`).replace("{f}", ext); } __name(makeImage, "makeImage"); function parseDuration(d) { const r = /* @__PURE__ */ __name((name, unit) => `((?<${name}>-?\\d*[\\.,]?\\d+)${unit})?`, "r"); const regex = new RegExp( [ "(?<negative>-)?P", r("years", "Y"), r("months", "M"), r("weeks", "W"), r("days", "D"), "(T", r("hours", "H"), r("minutes", "M"), r("seconds", "S"), ")?" // end optional time ].join("") ); const test = regex.exec(d); if (!test || !test.groups) return "0:00"; const dur = [ test.groups.years, test.groups.months, test.groups.weeks, test.groups.days, test.groups.hours, test.groups.minutes, test.groups.seconds ]; return dur.filter((r2, i, a) => !!r2 || i > a.length - 2).map((m, i) => { if (!m) m = "0"; return i < 1 ? m : m.padStart(2, "0"); }).join(":") || "0:00"; } __name(parseDuration, "parseDuration"); var _AppleMusic = class _AppleMusic { constructor() { return _AppleMusic; } static async search(query) { try { const url = `https://music.apple.com/us/search?term=${encodeURIComponent( query )}`; const node = await getHTML(url); if (!node) return []; const rawData = node.getElementById("serialized-server-data"); if (!rawData) return []; const data = JSON.parse(rawData.innerText)[0].data.sections; const tracks = data.find((s) => s.itemKind === "trackLockup")?.items; if (!tracks) return []; return tracks.map((track) => ({ id: track.contentDescriptor.identifiers.storeAdamID, duration: track.duration || "0:00", title: track.title, url: track.contentDescriptor.url, thumbnail: track?.artwork?.dictionary ? makeImage({ url: track.artwork.dictionary.url, height: track.artwork.dictionary.height, width: track.artwork.dictionary.width }) : "https://music.apple.com/assets/favicon/favicon-180.png", artist: { name: track.subtitleLinks?.[0]?.title ?? "Unknown Artist" } })); } catch { return []; } } static async getSongInfoFallback(res, name, id, link) { try { const metaTags = res.getElementsByTagName("meta"); if (!metaTags.length) return null; const title = metaTags.find((r) => r.getAttribute("name") === "apple:title")?.getAttribute("content") || res.querySelector("title")?.innerText || name; const contentId = metaTags.find((r) => r.getAttribute("name") === "apple:content_id")?.getAttribute("content") || id; const durationRaw = metaTags.find((r) => r.getAttribute("property") === "music:song:duration")?.getAttribute("content"); const song = { id: contentId, duration: durationRaw ? parseDuration(durationRaw) : metaTags.find((m) => m.getAttribute("name") === "apple:description")?.textContent.split("Duration: ")?.[1].split('"')?.[0] || "0:00", title, url: link, thumbnail: metaTags.find( (r) => ["og:image:secure_url", "og:image"].includes( r.getAttribute("property") ) )?.getAttribute("content") || "https://music.apple.com/assets/favicon/favicon-180.png", artist: { name: res.querySelector(".song-subtitles__artists>a")?.textContent?.trim() || "Apple Music" } }; return song; } catch { return null; } } static async getSongInfo(link) { if (!QueryResolver.regex.appleMusicSongRegex.test(link)) { return null; } const url = new URL(link); const id = url.searchParams.get("i"); const name = url.pathname.split("album/")[1]?.split("/")[0]; if (!id || !name) return null; const res = await getHTML(`https://music.apple.com/us/song/${name}/${id}`); if (!res) return null; try { const datasrc = res.getElementById("serialized-server-data")?.innerText || res.innerText.split( '<script type="application/json" id="serialized-server-data">' )?.[1]?.split("</script>")?.[0]; if (!datasrc) throw "not found"; const data = JSON.parse(datasrc)[0].data.seoData; const song = data.ogSongs[0]?.attributes; return { id: data.ogSongs[0]?.id || data.appleContentId || id, duration: song?.durationInMillis || "0:00", title: song?.name || data.appleTitle, url: song?.url || data.url || link, thumbnail: song?.artwork ? makeImage({ url: song.artwork.url, height: song.artwork.height, width: song.artwork.width }) : data.artworkUrl ? makeImage({ height: data.height, width: data.width, url: data.artworkUrl, ext: data.fileType || "jpg" }) : "https://music.apple.com/assets/favicon/favicon-180.png", artist: { name: song?.artistName || data.socialTitle || "Apple Music" } }; } catch { return this.getSongInfoFallback(res, name, id, link); } } static async getPlaylistInfo(link) { if (!QueryResolver.regex.appleMusicPlaylistRegex.test(link)) { return null; } const res = await getHTML(link); if (!res) return null; try { const datasrc = res.getElementById("serialized-server-data")?.innerText || res.innerText.split( '<script type="application/json" id="serialized-server-data">' )?.[1]?.split("</script>")?.[0]; if (!datasrc) throw "not found"; const pl = JSON.parse(datasrc)[0].data.seoData; const thumbnail = pl.artworkUrl ? makeImage({ height: pl.height, width: pl.width, url: pl.artworkUrl, ext: pl.fileType || "jpg" }) : "https://music.apple.com/assets/favicon/favicon-180.png"; return { id: pl.appleContentId, title: pl.appleTitle, thumbnail, artist: { name: pl.ogSongs?.[0]?.attributes?.artistName || "Apple Music" }, url: pl.url, tracks: ( // eslint-disable-next-line pl.ogSongs?.map((m) => { const song = m.attributes; return { id: m.id, duration: song.durationInMillis || "0:00", title: song.name, url: song.url, thumbnail: song.artwork ? makeImage({ url: song.artwork.url, height: song.artwork.height, width: song.artwork.width }) : thumbnail, artist: { name: song.artistName || "Apple Music" } }; }) || [] ) }; } catch { return null; } } static async getAlbumInfo(link) { if (!QueryResolver.regex.appleMusicAlbumRegex.test(link)) { return null; } const res = await getHTML(link); if (!res) return null; try { const datasrc = res.getElementById("serialized-server-data")?.innerText || res.innerText.split( '<script type="application/json" id="serialized-server-data">' )?.[1]?.split("</script>")?.[0]; if (!datasrc) throw "not found"; const pl = JSON.parse(datasrc)[0].data.seoData; const thumbnail = pl.artworkUrl ? makeImage({ height: pl.height, width: pl.width, url: pl.artworkUrl, ext: pl.fileType || "jpg" }) : "https://music.apple.com/assets/favicon/favicon-180.png"; return { id: pl.appleContentId, title: pl.appleTitle, thumbnail, artist: { name: pl.ogSongs?.[0]?.attributes?.artistName || "Apple Music" }, url: pl.url, tracks: ( // eslint-disable-next-line pl.ogSongs?.map((m) => { const song = m.attributes; return { id: m.id, duration: song.durationInMillis || "0:00", title: song.name, url: song.url, thumbnail: song.artwork ? makeImage({ url: song.artwork.url, height: song.artwork.height, width: song.artwork.width }) : thumbnail, artist: { name: song.artistName || "Apple Music" } }; }) || [] ) }; } catch { return null; } } }; __name(_AppleMusic, "AppleMusic"); var AppleMusic = _AppleMusic; // src/internal/Spotify.ts var SP_ANON_TOKEN_URL = "https://open.spotify.com/get_access_token?reason=transport&productType=embed"; var SP_ACCESS_TOKEN_URL = "https://accounts.spotify.com/api/token?grant_type=client_credentials"; var SP_BASE = "https://api.spotify.com/v1"; var _SpotifyAPI = class _SpotifyAPI { constructor(credentials = { clientId: null, clientSecret: null }) { this.credentials = credentials; __publicField(this, "accessToken", null); } get authorizationKey() { if (!this.credentials.clientId || !this.credentials.clientSecret) return null; return Buffer.from( `${this.credentials.clientId}:${this.credentials.clientSecret}` ).toString("base64"); } async requestToken() { const key = this.authorizationKey; if (!key) return await this.requestAnonymousToken(); try { const res = await fetch(SP_ACCESS_TOKEN_URL, { method: "POST", headers: { "User-Agent": UA, Authorization: `Basic ${key}`, "Content-Type": "application/json" } }); const body = await res.json(); if (!body.access_token) throw "no token"; const data = { token: body.access_token, expiresAfter: body.expires_in, type: "Bearer" }; return this.accessToken = data; } catch { return await this.requestAnonymousToken(); } } async requestAnonymousToken() { try { const res = await fetch(SP_ANON_TOKEN_URL, { headers: { "User-Agent": UA, "Content-Type": "application/json" } }); if (!res.ok) throw "not_ok"; const body = await res.json(); if (!body.accessToken) throw "no_access_token"; const data = { token: body.accessToken, expiresAfter: body.accessTokenExpirationTimestampMs, type: "Bearer" }; return this.accessToken = data; } catch { return null; } } isTokenExpired() { if (!this.accessToken) return true; return Date.now() > this.accessToken.expiresAfter; } async search(query) { try { if (this.isTokenExpired()) await this.requestToken(); if (!this.accessToken) return null; const res = await fetch( `${SP_BASE}/search/?q=${encodeURIComponent( query )}&type=track&market=US`, { headers: { "User-Agent": UA, Authorization: `${this.accessToken.type} ${this.accessToken.token}`, "Content-Type": "application/json" } } ); if (!res.ok) return null; const data = await res.json(); return data.tracks.items.map((m) => ({ title: m.name, duration: m.duration_ms, artist: m.artists.map((m2) => m2.name).join(", "), url: m.external_urls?.spotify || `https://open.spotify.com/track/${m.id}`, thumbnail: m.album.images?.[0]?.url || null })); } catch { return null; } } async getPlaylist(id) { try { if (this.isTokenExpired()) await this.requestToken(); if (!this.accessToken) return null; const res = await fetch(`${SP_BASE}/playlists/${id}?market=US`, { headers: { "User-Agent": UA, Authorization: `${this.accessToken.type} ${this.accessToken.token}`, "Content-Type": "application/json" } }); if (!res.ok) return null; const data = await res.json(); if (!data.tracks.items.length) return null; const t = data.tracks.items; let next = data.tracks.next; while (typeof next === "string") { try { const res2 = await fetch(next, { headers: { "User-Agent": UA, Authorization: `${this.accessToken.type} ${this.accessToken.token}`, "Content-Type": "application/json" } }); if (!res2.ok) break; const nextPage = await res2.json(); t.push(...nextPage.items); next = nextPage.next; if (!next) break; } catch { break; } } const tracks = t.map(({ track: m }) => ({ title: m.name, duration: m.duration_ms, artist: m.artists.map((m2) => m2.name).join(", "), url: m.external_urls?.spotify || `https://open.spotify.com/track/${m.id}`, thumbnail: m.album.images?.[0]?.url || null })); if (!tracks.length) return null; return { name: data.name, author: data.owner.display_name, thumbnail: data.images?.[0]?.url || null, id: data.id, url: data.external_urls.spotify || `https://open.spotify.com/playlist/${id}`, tracks }; } catch { return null; } } async getAlbum(id) { try { if (this.isTokenExpired()) await this.requestToken(); if (!this.accessToken) return null; const res = await fetch(`${SP_BASE}/albums/${id}?market=US`, { headers: { "User-Agent": UA, Authorization: `${this.accessToken.type} ${this.accessToken.token}`, "Content-Type": "application/json" } }); if (!res.ok) return null; const data = await res.json(); if (!data.tracks.items.length) return null; const t = data.tracks.items; let next = data.tracks.next; while (typeof next === "string") { try { const res2 = await fetch(next, { headers: { "User-Agent": UA, Authorization: `${this.accessToken.type} ${this.accessToken.token}`, "Content-Type": "application/json" } }); if (!res2.ok) break; const nextPage = await res2.json(); t.push(...nextPage.items); next = nextPage.next; if (!next) break; } catch { break; } } const tracks = t.map((m) => ({ title: m.name, duration: m.duration_ms, artist: m.artists.map((m2) => m2.name).join(", "), url: m.external_urls?.spotify || `https://open.spotify.com/track/${m.id}`, thumbnail: data.images?.[0]?.url || null })); if (!tracks.length) return null; return { name: data.name, author: data.artists.map((m) => m.name).join(", "), thumbnail: data.images?.[0]?.url || null, id: data.id, url: data.external_urls.spotify || `https://open.spotify.com/album/${id}`, tracks }; } catch { return null; } } }; __name(_SpotifyAPI, "SpotifyAPI"); var SpotifyAPI = _SpotifyAPI; // src/extractors/AppleMusicExtractor.ts var _AppleMusicExtractor = class _AppleMusicExtractor extends BaseExtractor5 { constructor() { super(...arguments); __publicField(this, "_stream"); } async activate() { this.protocols = ["amsearch", "applemusic"]; const fn = this.options.createStream; if (typeof fn === "function") { this._stream = (q, t) => { return fn(this, q, t); }; } } async deactivate() { this.protocols = []; } async validate(query, type) { return [ QueryType5.APPLE_MUSIC_ALBUM, QueryType5.APPLE_MUSIC_PLAYLIST, QueryType5.APPLE_MUSIC_SONG, QueryType5.APPLE_MUSIC_SEARCH, QueryType5.AUTO, QueryType5.AUTO_SEARCH ].some((t) => t === type); } async getRelatedTracks(track, history) { if (track.queryType === QueryType5.APPLE_MUSIC_SONG) { const data = await this.handle(track.author || track.title, { type: QueryType5.APPLE_MUSIC_SEARCH, requestedBy: track.requestedBy }); const unique = data.tracks.filter( (t) => !history.tracks.some((h) => h.url === t.url) ); return unique.length > 0 ? this.createResponse(null, unique) : this.createResponse(); } return this.createResponse(); } async handle(query, context) { if (context.protocol === "amsearch") context.type = QueryType5.APPLE_MUSIC_SEARCH; switch (context.type) { case QueryType5.AUTO: case QueryType5.AUTO_SEARCH: case QueryType5.APPLE_MUSIC_SEARCH: { const data = await AppleMusic.search(query); if (!data || !data.length) return this.createResponse(); const tracks = data.map( // eslint-disable-next-line @typescript-eslint/no-explicit-any (m) => { const track = new Track5(this.context.player, { author: m.artist.name, description: m.title, duration: typeof m.duration === "number" ? Util5.buildTimeCode(Util5.parseMS(m.duration)) : m.duration, thumbnail: m.thumbnail, title: m.title, url: m.url, views: 0, source: "apple_music", requestedBy: context.requestedBy, queryType: "appleMusicSong", metadata: { source: m, bridge: null }, requestMetadata: /* @__PURE__ */ __name(async () => { return { source: m, bridge: null }; }, "requestMetadata") }); track.extractor = this; return track; } ); return this.createResponse(null, tracks); } case QueryType5.APPLE_MUSIC_ALBUM: { const info = await AppleMusic.getAlbumInfo(query); if (!info) return this.createResponse(); const playlist = new Playlist2(this.context.player, { author: { name: info.artist.name, url: "" }, description: info.title, id: info.id, source: "apple_music", thumbnail: info.thumbnail, title: info.title, tracks: [], type: "album", url: info.url, rawPlaylist: info }); playlist.tracks = info.tracks.map( (m) => { const track = new Track5(this.context.player, { author: m.artist.name, description: m.title, duration: typeof m.duration === "number" ? Util5.buildTimeCode(Util5.parseMS(m.duration)) : m.duration, thumbnail: m.thumbnail, title: m.title, url: m.url, views: 0, source: "apple_music", requestedBy: context.requestedBy, queryType: "appleMusicSong", metadata: { source: info, bridge: null }, requestMetadata: /* @__PURE__ */ __name(async () => { return { source: info, bridge: null }; }, "requestMetadata") }); track.playlist = playlist; track.extractor = this; return track; } ); return { playlist, tracks: playlist.tracks }; } case QueryType5.APPLE_MUSIC_PLAYLIST: { const info = await AppleMusic.getPlaylistInfo(query); if (!info) return this.createResponse(); const playlist = new Playlist2(this.context.player, { author: { name: info.artist.name, url: "" }, description: info.title, id: info.id, source: "apple_music", thumbnail: info.thumbnail, title: info.title, tracks: [], type: "playlist", url: info.url, rawPlaylist: info }); playlist.tracks = info.tracks.map( (m) => { const track = new Track5(this.context.player, { author: m.artist.name, description: m.title, duration: typeof m.duration === "number" ? Util5.buildTimeCode(Util5.parseMS(m.duration)) : m.duration, thumbnail: m.thumbnail, title: m.title, url: m.url, views: 0, source: "apple_music", requestedBy: context.requestedBy, queryType: "appleMusicSong", metadata: { source: m, bridge: null }, requestMetadata: /* @__PURE__ */ __name(async () => { return { source: m, bridge: null }; }, "requestMetadata") }); track.playlist = playlist; track.extractor = this; return track; } ); return { playlist, tracks: playlist.tracks }; } case QueryType5.APPLE_MUSIC_SONG: { const info = await AppleMusic.getSongInfo(query); if (!info) return this.createResponse(); const track = new Track5(this.context.player, { author: info.artist.name, description: info.title, duration: typeof info.duration === "number" ? Util5.buildTimeCode(Util5.parseMS(info.duration)) : info.duration, thumbnail: info.thumbnail, title: info.title, url: info.url, views: 0, source: "apple_music", requestedBy: context.requestedBy, queryType: context.type, metadata: { source: info, bridge: null }, requestMetadata: /* @__PURE__ */ __name(async () => { return { source: info, bridge: null }; }, "requestMetadata") }); track.extractor = this; return { playlist: null, tracks: [track] }; } default: return { playlist: null, tracks: [] }; } } async stream(info) { if (this._stream) { const stream = await this._stream(info.url, info); if (typeof stream === "string") return stream; return stream; } const result = await this.context.requestBridge(info, this); if (!result?.result) throw new Error("Could not bridge this track"); return result.result; } }; __name(_AppleMusicExtractor, "AppleMusicExtractor"); __publicField(_AppleMusicExtractor, "identifier", "com.discord-player.applemusicextractor"); var AppleMusicExtractor = _AppleMusicExtractor; // src/extractors/SpotifyExtractor.ts import { BaseExtractor as BaseExtractor6, Playlist as Playlist3, QueryType as QueryType6, Track as Track6, Util as Util6 } from "discord-player"; import spotify from "spotify-url-info"; var re = /^(?:https:\/\/open\.spotify\.com\/(intl-([a-z]|[A-Z]){0,3}\/)?(?:user\/[A-Za-z0-9]+\/)?|spotify:)(album|playlist|track)(?:[/:])([A-Za-z0-9]+).*$/; var _SpotifyExtractor = class _SpotifyExtractor extends BaseExtractor6 { constructor() { super(...arguments); __publicField(this, "_stream"); __publicField(this, "_lib"); __publicField(this, "_credentials", { clientId: this.options.clientId || process.env.DP_SPOTIFY_CLIENT_ID || null, clientSecret: this.options.clientSecret || process.env.DP_SPOTIFY_CLIENT_SECRET || null }); __publicField(this, "internal", new SpotifyAPI(this._credentials)); } async activate() { this.protocols = ["spsearch", "spotify"]; this._lib = spotify(fetch); if (this.internal.isTokenExpired()) await this.internal.requestToken(); const fn = this.options.createStream; if (typeof fn === "function") { this._stream = (q) => { return fn(this, q); }; } } async deactivate() { this._stream = void 0; this._lib = void 0; this.protocols = []; } async validate(query, type) { return [ QueryType6.SPOTIFY_ALBUM, QueryType6.SPOTIFY_PLAYLIST, QueryType6.SPOTIFY_SONG, QueryType6.SPOTIFY_SEARCH, QueryType6.AUTO, QueryType6.AUTO_SEARCH ].some((t) => t === type); } async getRelatedTracks(track) { return await this.handle(track.author || track.title, { type: QueryType6.SPOTIFY_SEARCH, requestedBy: track.requestedBy }); } async handle(query, context) { if (context.protocol === "spsearch") context.type = QueryType6.SPOTIFY_SEARCH; switch (context.type) { case QueryType6.AUTO: case QueryType6.AUTO_SEARCH: case QueryType6.SPOTIFY_SEARCH: { const data = await this.internal.search(query); if (!data) return this.createResponse(); return this.createResponse( null, data.map((spotifyData) => { const track = new Track6(this.context.player, { title: spotifyData.title, description: `${spotifyData.title} by ${spotifyData.artist}`, author: spotifyData.artist ?? "Unknown Artist", url: spotifyData.url, thumbnail: spotifyData.thumbnail || "https://www.scdn.co/i/_global/twitter_card-default.jpg", duration: Util6.buildTimeCode( Util6.parseMS(spotifyData.duration ?? 0) ), views: 0, requestedBy: context.requestedBy, source: "spotify", queryType: QueryType6.SPOTIFY_SONG, metadata: { source: spotifyData, bridge: null }, requestMetadata: /* @__PURE__ */ __name(async () => { return { source: spotifyData, bridge: null }; }, "requestMetadata") }); track.extractor = this; return track; }) ); } case QueryType6.SPOTIFY_SONG: { const spotifyData = await this._lib.getData(query, context.requestOptions).catch(Util6.noop); if (!spotifyData) return { playlist: null, tracks: [] }; const spotifyTrack = new Track6(this.context.player, { title: spotifyData.title, description: `${spotifyData.name} by ${spotifyData.artists.map((m) => m.name).join(", ")}`, author: spotifyData.artists[0]?.name ?? "Unknown Artist", url: spotifyData.id ? `https://open.spotify.com/track/${spotifyData.id}` : query, thumbnail: spotifyData.coverArt?.sources?.[0]?.url || "https://www.scdn.co/i/_global/twitter_card-default.jpg", duration: Util6.buildTimeCode( Util6.parseMS(spotifyData.duration ?? spotifyData.maxDuration ?? 0) ), views: 0, requestedBy: context.requestedBy, source: "spotify", quer