@discord-player/extractor
Version:
Extractors for discord-player
1,461 lines (1,444 loc) • 203 kB
JavaScript
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