@discord-player/extractor
Version:
Extractors for discord-player
1,371 lines (1,352 loc) • 206 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __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 __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/index.ts
var src_exports = {};
__export(src_exports, {
AppleMusicExtractor: () => AppleMusicExtractor,
AttachmentExtractor: () => AttachmentExtractor,
DefaultExtractors: () => DefaultExtractors,
Internal: () => internal_exports,
ReverbnationExtractor: () => ReverbnationExtractor,
SoundCloudExtractor: () => SoundCloudExtractor,
SpotifyExtractor: () => SpotifyExtractor,
VimeoExtractor: () => VimeoExtractor,
lyricsExtractor: () => lyricsExtractor,
version: () => version
});
module.exports = __toCommonJS(src_exports);
// src/extractors/SoundCloudExtractor.ts
var import_discord_player = require("discord-player");
var SoundCloud = __toESM(require("soundcloud.ts"));
// src/internal/helper.ts
var import_isomorphic_unfetch = __toESM(require("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 = import_isomorphic_unfetch.default;
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 import_discord_player.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 [
import_discord_player.QueryType.SOUNDCLOUD,
import_discord_player.QueryType.SOUNDCLOUD_PLAYLIST,
import_discord_player.QueryType.SOUNDCLOUD_SEARCH,
import_discord_player.QueryType.SOUNDCLOUD_TRACK,
import_discord_player.QueryType.AUTO,
import_discord_player.QueryType.AUTO_SEARCH
].some((r) => r === type);
}
async getRelatedTracks(track, history) {
if (track.queryType === import_discord_player.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 import_discord_player.Track(this.context.player, {
title: trackInfo.title,
url: trackInfo.permalink_url,
duration: import_discord_player.Util.buildTimeCode(import_discord_player.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: import_discord_player.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 = import_discord_player.QueryType.SOUNDCLOUD_SEARCH;
switch (context.type) {
case import_discord_player.QueryType.SOUNDCLOUD_TRACK: {
const trackInfo = await this.internal.tracks.getV2(query).catch(import_discord_player.Util.noop);
if (!trackInfo) return this.emptyResponse();
const track = new import_discord_player.Track(this.context.player, {
title: trackInfo.title,
url: trackInfo.permalink_url,
duration: import_discord_player.Util.buildTimeCode(import_discord_player.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 import_discord_player.QueryType.SOUNDCLOUD_PLAYLIST: {
const data = await this.internal.playlists.getV2(query).catch(import_discord_player.Util.noop);
if (!data) return { playlist: null, tracks: [] };
const res = new import_discord_player.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 import_discord_player.Track(this.context.player, {
title: song.title,
description: song.description ?? "",
author: song.user.username,
url: song.permalink_url,
thumbnail: song.artwork_url,
duration: import_discord_player.Util.buildTimeCode(import_discord_player.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(import_discord_player.Util.noop);
if (!tracks)
tracks = await this.internal.tracks.searchAlt(query).catch(import_discord_player.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 import_discord_player.Track(this.context.player, {
title: trackInfo.title,
url: trackInfo.permalink_url,
duration: import_discord_player.Util.buildTimeCode(import_discord_player.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(import_discord_player.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: import_discord_player.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
var import_discord_player2 = require("discord-player");
// src/internal/Vimeo.ts
var import_http = __toESM(require("http"));
var import_https = __toESM(require("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://") ? import_https.default : import_http.default;
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 import_discord_player2.BaseExtractor {
async validate(query, type) {
if (typeof query !== "string") return false;
return [import_discord_player2.QueryType.VIMEO].some((r) => r === type);
}
async getRelatedTracks(track) {
return this.createResponse();
}
async handle(query, context) {
switch (context.type) {
case import_discord_player2.QueryType.VIMEO: {
const trackInfo = await Vimeo.getInfo(
query.split("/").filter((x) => !!x).pop()
).catch(import_discord_player2.Util.noop);
if (!trackInfo) return this.emptyResponse();
const track = new import_discord_player2.Track(this.context.player, {
title: trackInfo.title,
url: trackInfo.url,
duration: import_discord_player2.Util.buildTimeCode(import_discord_player2.Util.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(import_discord_player2.Util.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
var import_discord_player3 = require("discord-player");
var import_reverbnation_scraper = __toESM(require("reverbnation-scraper"));
var _ReverbnationExtractor = class _ReverbnationExtractor extends import_discord_player3.BaseExtractor {
async validate(query, type) {
if (typeof query !== "string") return false;
return [import_discord_player3.QueryType.REVERBNATION].some(
(r) => r === type
);
}
async getRelatedTracks(track) {
return this.createResponse();
}
async handle(query, context) {
switch (context.type) {
case import_discord_player3.QueryType.REVERBNATION: {
const trackInfo = await import_reverbnation_scraper.default.getInfo(query).catch(import_discord_player3.Util.noop);
if (!trackInfo) return this.emptyResponse();
const track = new import_discord_player3.Track(this.context.player, {
title: trackInfo.title,
url: trackInfo.url,
duration: import_discord_player3.Util.buildTimeCode(import_discord_player3.Util.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 import_reverbnation_scraper.default.getInfo(info.url).catch(import_discord_player3.Util.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
var import_discord_player4 = require("discord-player");
var import_fs = require("fs");
// src/internal/downloader.ts
var import_http2 = __toESM(require("http"));
var import_https2 = __toESM(require("https"));
function downloadStream(url, opts = {}) {
return new Promise((resolve, reject) => {
const lib = url.startsWith("http://") ? import_http2.default : import_https2.default;
lib.get(url, opts, (res) => resolve(res)).once("error", reject);
});
}
__name(downloadStream, "downloadStream");
// src/extractors/AttachmentExtractor.ts
var fileType = __toESM(require("file-type"));
var import_path = __toESM(require("path"));
var import_promises = require("fs/promises");
var ATTACHMENT_HEADER = ["audio/", "video/", "application/ogg"];
var _AttachmentExtractor = class _AttachmentExtractor extends import_discord_player4.BaseExtractor {
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 [import_discord_player4.QueryType.ARBITRARY, import_discord_player4.QueryType.FILE].some(
(r) => r === type
);
}
async getRelatedTracks(track) {
return this.createResponse();
}
async handle(query, context) {
switch (context.type) {
case import_discord_player4.QueryType.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 import_discord_player4.Track(this.context.player, {
title: trackInfo.title,
url: trackInfo.url,
duration: import_discord_player4.Util.buildTimeCode(import_discord_player4.Util.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 import_discord_player4.QueryType.FILE: {
if (!(0, import_fs.existsSync)(query)) return this.emptyResponse();
const fstat = await (0, import_promises.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: import_path.default.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(
(0, import_fs.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 import_discord_player4.Track(this.context.player, {
title: trackInfo.title,
url: trackInfo.url,
duration: import_discord_player4.Util.buildTimeCode(import_discord_player4.Util.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 (0, import_fs.createReadStream)(engine);
}
};
__name(_AttachmentExtractor, "AttachmentExtractor");
__publicField(_AttachmentExtractor, "identifier", "com.discord-player.attachmentextractor");
var AttachmentExtractor = _AttachmentExtractor;
// src/extractors/AppleMusicExtractor.ts
var import_discord_player6 = require("discord-player");
// src/internal/index.ts
var internal_exports = {};
__export(internal_exports, {
AppleMusic: () => AppleMusic,
SpotifyAPI: () => SpotifyAPI,
Vimeo: () => Vimeo,
downloadStream: () => downloadStream
});
// src/internal/AppleMusic.ts
var import_discord_player5 = require("discord-player");
var import_node_html_parser = require("node-html-parser");
function getHTML(link) {
return fetch(link, {
headers: {
"User-Agent": UA
}
}).then((r) => r.text()).then(
(txt) => (0, import_node_html_parser.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 (!import_discord_player5.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 (!import_discord_player5.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 (!import_discord_player5.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 import_discord_player6.BaseExtractor {
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 [
import_discord_player6.QueryType.APPLE_MUSIC_ALBUM,
import_discord_player6.QueryType.APPLE_MUSIC_PLAYLIST,
import_discord_player6.QueryType.APPLE_MUSIC_SONG,
import_discord_player6.QueryType.APPLE_MUSIC_SEARCH,
import_discord_player6.QueryType.AUTO,
import_discord_player6.QueryType.AUTO_SEARCH
].some((t) => t === type);
}
async getRelatedTracks(track, history) {
if (track.queryType === import_discord_player6.QueryType.APPLE_MUSIC_SONG) {
const data = await this.handle(track.author || track.title, {
type: import_discord_player6.QueryType.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 = import_discord_player6.QueryType.APPLE_MUSIC_SEARCH;
switch (context.type) {
case import_discord_player6.QueryType.AUTO:
case import_discord_player6.QueryType.AUTO_SEARCH:
case import_discord_player6.QueryType.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 import_discord_player6.Track(this.context.player, {
author: m.artist.name,
description: m.title,
duration: typeof m.duration === "number" ? import_discord_player6.Util.buildTimeCode(import_discord_player6.Util.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 import_discord_player6.QueryType.APPLE_MUSIC_ALBUM: {
const info = await AppleMusic.getAlbumInfo(query);
if (!info) return this.createResponse();
const playlist = new import_discord_player6.Playlist(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 import_discord_player6.Track(this.context.player, {
author: m.artist.name,
description: m.title,
duration: typeof m.duration === "number" ? import_discord_player6.Util.buildTimeCode(import_discord_player6.Util.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 import_discord_player6.QueryType.APPLE_MUSIC_PLAYLIST: {
const info = await AppleMusic.getPlaylistInfo(query);
if (!info) return this.createResponse();
const playlist = new import_discord_player6.Playlist(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 import_discord_player6.Track(this.context.player, {
author: m.artist.name,
description: m.title,
duration: typeof m.duration === "number" ? import_discord_player6.Util.buildTimeCode(import_discord_player6.Util.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 import_discord_player6.QueryType.APPLE_MUSIC_SONG: {
const info = await AppleMusic.getSongInfo(query);
if (!info) return this.createResponse();
const track = new import_discord_player6.Track(this.context.player, {
author: info.artist.name,
description: info.title,
duration: typeof info.duration === "number" ? import_discord_player6.Util.buildTimeCode(import_discord_player6.Util.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
var import_discord_player7 = require("discord-player");
var import_spotify_url_info = __toESM(require("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 import_discord_player7.BaseExtractor {
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
});