323 lines
11 KiB
JavaScript
323 lines
11 KiB
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 __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);
|
|
|
|
// src/index.ts
|
|
var src_exports = {};
|
|
__export(src_exports, {
|
|
SpotifyPlugin: () => SpotifyPlugin,
|
|
default: () => src_default
|
|
});
|
|
module.exports = __toCommonJS(src_exports);
|
|
|
|
// src/API.ts
|
|
var import_spotify_url_info = __toESM(require("spotify-url-info"));
|
|
var import_spotify_web_api_node = __toESM(require("spotify-web-api-node"));
|
|
var import_undici = require("undici");
|
|
var import_distube = require("distube");
|
|
var import_spotify_uri = require("spotify-uri");
|
|
var SUPPORTED_TYPES = ["album", "playlist", "track", "artist"];
|
|
var WEB_API = new import_spotify_web_api_node.default();
|
|
var INFO = (0, import_spotify_url_info.default)(import_undici.fetch);
|
|
var firstWarning1 = true;
|
|
var firstWarning2 = true;
|
|
var apiError = /* @__PURE__ */ __name((e) => new import_distube.DisTubeError(
|
|
"SPOTIFY_API_ERROR",
|
|
`The URL is private or unavailable.${e?.body?.error?.message ? `
|
|
Details: ${e.body.error.message}` : ""}${e?.statusCode ? `
|
|
Status code: ${e.statusCode}.` : ""}`
|
|
), "apiError");
|
|
var APITrack = class {
|
|
static {
|
|
__name(this, "APITrack");
|
|
}
|
|
type;
|
|
id;
|
|
name;
|
|
artists;
|
|
thumbnail;
|
|
duration;
|
|
constructor(info) {
|
|
this.type = "track";
|
|
this.id = info.id;
|
|
this.name = info.name;
|
|
this.artists = info.artists;
|
|
this.thumbnail = info.album?.images?.[0]?.url;
|
|
this.duration = info.duration_ms;
|
|
}
|
|
};
|
|
var mergeAlbumTrack = /* @__PURE__ */ __name((album, track) => {
|
|
track.album = album;
|
|
return track;
|
|
}, "mergeAlbumTrack");
|
|
var API = class {
|
|
static {
|
|
__name(this, "API");
|
|
}
|
|
_hasCredentials = false;
|
|
_expirationTime = 0;
|
|
_tokenAvailable = false;
|
|
topTracksCountry = "US";
|
|
constructor(clientId, clientSecret, topTracksCountry) {
|
|
if (clientId && clientSecret) {
|
|
this._hasCredentials = true;
|
|
WEB_API.setClientId(clientId);
|
|
WEB_API.setClientSecret(clientSecret);
|
|
}
|
|
if (topTracksCountry) {
|
|
if (!/^[A-Z]{2}$/.test(topTracksCountry)) throw new Error("Invalid region code");
|
|
this.topTracksCountry = topTracksCountry;
|
|
}
|
|
}
|
|
isSupportedTypes(type) {
|
|
return SUPPORTED_TYPES.includes(type);
|
|
}
|
|
async refreshToken() {
|
|
if (Date.now() < this._expirationTime) return;
|
|
if (this._hasCredentials) {
|
|
try {
|
|
const { body } = await WEB_API.clientCredentialsGrant();
|
|
WEB_API.setAccessToken(body.access_token);
|
|
this._expirationTime = Date.now() + body.expires_in * 1e3 - 5e3;
|
|
} catch (e) {
|
|
if (firstWarning1) {
|
|
firstWarning1 = false;
|
|
this._hasCredentials = false;
|
|
console.warn(e);
|
|
console.warn("[SPOTIFY_PLUGIN_API] Cannot get token from your credentials. Try scraping token instead.");
|
|
}
|
|
}
|
|
}
|
|
if (!this._hasCredentials) {
|
|
const response = await (0, import_undici.fetch)("https://open.spotify.com/");
|
|
const body = await response.text();
|
|
const token = body.match(/"accessToken":"(.+?)"/)?.[1];
|
|
if (!token) {
|
|
this._tokenAvailable = false;
|
|
if (firstWarning2) {
|
|
firstWarning2 = false;
|
|
console.warn(
|
|
"[SPOTIFY_PLUGIN_API] Cannot get token from scraping. Cannot fetch more than 100 tracks from a playlist or album."
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
WEB_API.setAccessToken(token);
|
|
const expiration = body.match(/"accessTokenExpirationTimestampMs":(\d+)/)?.[1];
|
|
if (expiration) this._expirationTime = Number(expiration) - 5e3;
|
|
}
|
|
this._tokenAvailable = true;
|
|
}
|
|
parseUrl(url) {
|
|
return (0, import_spotify_uri.parse)(url);
|
|
}
|
|
async getData(url) {
|
|
const { type, id } = this.parseUrl(url);
|
|
if (!id) throw new import_distube.DisTubeError("SPOTIFY_API_INVALID_URL", "Invalid URL");
|
|
if (!this.isSupportedTypes(type)) throw new import_distube.DisTubeError("SPOTIFY_API_UNSUPPORTED_TYPE", "Unsupported URL type");
|
|
await this.refreshToken();
|
|
if (type === "track") {
|
|
if (!this._tokenAvailable) {
|
|
return INFO.getData(url);
|
|
}
|
|
try {
|
|
const { body } = await WEB_API.getTrack(id);
|
|
return new APITrack(body);
|
|
} catch (e) {
|
|
throw apiError(e);
|
|
}
|
|
}
|
|
if (!this._tokenAvailable) {
|
|
const data = await INFO.getData(url);
|
|
const thumbnail = data.coverArt?.sources?.[0]?.url;
|
|
return {
|
|
type,
|
|
name: data.title,
|
|
thumbnail,
|
|
url,
|
|
tracks: data.trackList.map((i) => ({
|
|
type: "track",
|
|
id: this.parseUrl(i.uri).id,
|
|
name: i.title,
|
|
artists: [{ name: i.subtitle }],
|
|
duration: i.duration,
|
|
thumbnail
|
|
}))
|
|
};
|
|
}
|
|
try {
|
|
const { body } = await WEB_API[type === "album" ? "getAlbum" : type === "playlist" ? "getPlaylist" : "getArtist"](id);
|
|
return {
|
|
type,
|
|
name: body.name,
|
|
thumbnail: body.images?.[0]?.url,
|
|
url: body.external_urls?.spotify,
|
|
tracks: (await this.#getTracks(body)).filter((t) => t?.type === "track").map((t) => new APITrack(t))
|
|
};
|
|
} catch (e) {
|
|
throw apiError(e);
|
|
}
|
|
}
|
|
async #getTracks(data) {
|
|
switch (data.type) {
|
|
case "artist": {
|
|
return (await WEB_API.getArtistTopTracks(data.id, this.topTracksCountry)).body.tracks;
|
|
}
|
|
case "album": {
|
|
const tracks = await this.#getPaginatedItems(data);
|
|
return tracks.map((t) => mergeAlbumTrack(data, t));
|
|
}
|
|
case "playlist": {
|
|
return (await this.#getPaginatedItems(data)).map((i) => i.track).filter(import_distube.isTruthy);
|
|
}
|
|
}
|
|
}
|
|
async #getPaginatedItems(data) {
|
|
const items = data.tracks.items;
|
|
const isPlaylist = data.type === "playlist";
|
|
const limit = isPlaylist ? 100 : 50;
|
|
const method = isPlaylist ? "getPlaylistTracks" : "getAlbumTracks";
|
|
while (data.tracks.next) {
|
|
await this.refreshToken();
|
|
data.tracks = (await WEB_API[method](data.id, { offset: data.tracks.offset + data.tracks.limit, limit })).body;
|
|
items.push(...data.tracks.items);
|
|
}
|
|
return items;
|
|
}
|
|
};
|
|
|
|
// src/index.ts
|
|
var import_distube2 = require("distube");
|
|
var SpotifyPlugin = class extends import_distube2.InfoExtractorPlugin {
|
|
static {
|
|
__name(this, "SpotifyPlugin");
|
|
}
|
|
api;
|
|
constructor(options = {}) {
|
|
super();
|
|
if (typeof options !== "object" || Array.isArray(options)) {
|
|
throw new import_distube2.DisTubeError("INVALID_TYPE", ["object", "undefined"], options, "SpotifyPluginOptions");
|
|
}
|
|
(0, import_distube2.checkInvalidKey)(options, ["api"], "SpotifyPluginOptions");
|
|
if (options.api !== void 0 && (typeof options.api !== "object" || Array.isArray(options.api))) {
|
|
throw new import_distube2.DisTubeError("INVALID_TYPE", ["object", "undefined"], options.api, "api");
|
|
} else if (options.api) {
|
|
if (options.api.clientId && typeof options.api.clientId !== "string") {
|
|
throw new import_distube2.DisTubeError("INVALID_TYPE", "string", options.api.clientId, "SpotifyPluginOptions.api.clientId");
|
|
}
|
|
if (options.api.clientSecret && typeof options.api.clientSecret !== "string") {
|
|
throw new import_distube2.DisTubeError(
|
|
"INVALID_TYPE",
|
|
"string",
|
|
options.api.clientSecret,
|
|
"SpotifyPluginOptions.api.clientSecret"
|
|
);
|
|
}
|
|
if (options.api.topTracksCountry && typeof options.api.topTracksCountry !== "string") {
|
|
throw new import_distube2.DisTubeError(
|
|
"INVALID_TYPE",
|
|
"string",
|
|
options.api.topTracksCountry,
|
|
"SpotifyPluginOptions.api.topTracksCountry"
|
|
);
|
|
}
|
|
}
|
|
this.api = new API(options.api?.clientId, options.api?.clientSecret, options.api?.topTracksCountry);
|
|
}
|
|
validate(url) {
|
|
if (typeof url !== "string" || !url.includes("spotify")) return false;
|
|
try {
|
|
const parsedURL = this.api.parseUrl(url);
|
|
if (!parsedURL.type || !this.api.isSupportedTypes(parsedURL.type)) return false;
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
async resolve(url, options) {
|
|
const data = await this.api.getData(url);
|
|
if (data.type === "track") {
|
|
return new import_distube2.Song(
|
|
{
|
|
plugin: this,
|
|
source: "spotify",
|
|
playFromSource: false,
|
|
name: data.name,
|
|
id: data.id,
|
|
url: `https://open.spotify.com/track/${data.id}`,
|
|
thumbnail: data.thumbnail,
|
|
uploader: {
|
|
name: data.artists.map((a) => a.name).join(", ")
|
|
},
|
|
duration: data.duration / 1e3
|
|
},
|
|
options
|
|
);
|
|
}
|
|
return new import_distube2.Playlist(
|
|
{
|
|
source: "spotify",
|
|
name: data.name,
|
|
url: data.url,
|
|
thumbnail: data.thumbnail,
|
|
songs: data.tracks.map(
|
|
(t) => new import_distube2.Song(
|
|
{
|
|
plugin: this,
|
|
source: "spotify",
|
|
id: t.id,
|
|
playFromSource: false,
|
|
name: t.name,
|
|
thumbnail: t.thumbnail,
|
|
uploader: {
|
|
name: t.artists.map((a) => a.name).join(", ")
|
|
},
|
|
url: `https://open.spotify.com/track/${t.id}`,
|
|
duration: t.duration / 1e3
|
|
},
|
|
options
|
|
)
|
|
)
|
|
},
|
|
options
|
|
);
|
|
}
|
|
createSearchQuery(song) {
|
|
return `${song.name} ${song.uploader.name}`;
|
|
}
|
|
getRelatedSongs() {
|
|
return [];
|
|
}
|
|
};
|
|
var src_default = SpotifyPlugin;
|
|
// Annotate the CommonJS export names for ESM import in node:
|
|
0 && (module.exports = {
|
|
SpotifyPlugin
|
|
});
|
|
//# sourceMappingURL=index.js.map
|