UNPKG

discord-player-applemusic

Version:
371 lines (364 loc) 13.6 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; 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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { AppleMusicExtractor: () => AppleMusicExtractor }); module.exports = __toCommonJS(index_exports); // src/AppleMusicExtractor.ts var import_discord_player = require("discord-player"); // src/internal/AppleMusicAPI.ts var import_node_html_parser = require("node-html-parser"); // src/internal/helper.ts 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 appleMusicSongRegex = /^https?:\/\/music\.apple\.com\/.+?\/(song\/.+|album\/.+?\?i=)([0-9]+)$/; var appleMusicAlbumRegex = /^https?:\/\/music\.apple\.com\/.+?\/album\/.+\/([0-9]+)$/; var appleMusicPlaylistRegex = /^https?:\/\/music\.apple\.com\/.+?\/playlist\/.+\/pl\.(u-|pm-)?[a-zA-Z0-9]+$/; var isUrl = (url) => { try { new URL(url); return true; } catch { return false; } }; // src/internal/AppleMusicAPI.ts function getHTML(link) { return fetch(link, { headers: { "User-Agent": UA } }).then((r) => r.text()).then( (txt) => (0, import_node_html_parser.parse)(txt), () => null ); } function makeImage({ height, url, width, ext = "jpg" }) { return url.replace("{w}", `${width}`).replace("{h}", `${height}`).replace("{f}", ext); } function parseDuration(d) { const r = (name, unit) => `((?<${name}>-?\\d*[\\.,]?\\d+)${unit})?`; 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((item, i, a) => !!item || i > a.length - 2).map((m, i) => { if (!m) m = "0"; return i < 1 ? m : m.padStart(2, "0"); }).join(":") || "0:00"; } var AppleMusic = class _AppleMusic { constructor() { return _AppleMusic; } /** * Search for tracks on Apple Music * @param query - The search query * @returns An array of track objects matching the search query */ 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: track.subtitleLinks?.[0]?.title ?? "Unknown Artist" })); } catch (e) { return []; } } /** * Gets information about a song from its Apple Music URL * @param link - The Apple Music song URL * @returns The song information object or null if not found */ static async getSongInfo(link) { const url = new URL(link); let id, name; if (url.pathname.includes("/album/")) { id = url.searchParams.get("i"); name = url.pathname.split("album/")[1]?.split("/")[0]; } else if (url.pathname.includes("/song/")) { const parts = url.pathname.split("/song/")[1]?.split("/"); name = parts?.[0]; id = parts?.[1]; } const songURL = `https://music.apple.com/us/song/${name}/${id}`; if (!id || !name) return null; const res = await getHTML(songURL); if (!res) return null; 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: title || "Unknown Title", url: songURL, 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: res.querySelector(".song-subtitles__artists>a")?.textContent?.trim() || "Apple Music" }; return song; } catch (err) { return null; } } /** * Gets information about a playlist from its Apple Music URL * @param link - The Apple Music playlist URL * @returns The playlist information object or null if not found */ static async getPlaylistInfo(link) { const root = await getHTML(link); if (!root) return null; const titleElement = root.querySelector('meta[property="og:title"]'); const title = titleElement ? titleElement.getAttribute("content") : "Unknown Title"; const descriptionElement = root.querySelector('meta[property="og:description"]'); const description = descriptionElement ? descriptionElement.getAttribute("content") : "No Description"; const artworkElement = root.querySelector('meta[property="og:image"]'); const artwork = artworkElement ? artworkElement.getAttribute("content") : ""; const tracks = []; const jsonLdElement = root.querySelector("script#schema\\:music-playlist"); let artistNames = []; const serializedScript = root.querySelector("script#serialized-server-data"); if (serializedScript) { try { const jsonData = JSON.parse(serializedScript.text); artistNames = jsonData[0].data.sections.find((section) => section.id.includes("track-list")).items.filter((item) => item.id.includes("track-lockup")).map((i) => i.artistName); } catch (error) { } } if (jsonLdElement) { const jsonLd = JSON.parse(jsonLdElement.text); if (jsonLd && jsonLd.track) { jsonLd.track.forEach((track, index) => { const artist = artistNames[index] || track.byArtist?.name || "Unknown Artist"; tracks.push({ id: track.url.split("/").pop() || "", title: track.name || "Unknown Title", artist, duration: parseDuration(track.duration || "PT0S"), url: track.url || "", thumbnail: "https://music.apple.com/assets/favicon/favicon-180.png" // Default thumbnail }); }); } } return { title: title || "Unknown Album", description, artwork: artwork || "https://music.apple.com/assets/favicon/favicon-180.png", tracks, url: link }; } /** * Gets information about an album from its Apple Music URL * @param link - The Apple Music album URL * @returns The album information object or null if not found */ static async getAlbumInfo(link) { const root = await getHTML(link.split("?")[0]); if (!root) return null; try { const titleElement = root.querySelector('meta[name="apple:title"]'); const title = titleElement ? titleElement.getAttribute("content") : "Unknown Album"; const artworkElement = root.querySelector('meta[property="og:image"]'); const thumbnail = artworkElement ? artworkElement.getAttribute("content") : "https://music.apple.com/assets/favicon/favicon-180.png"; const tracks = []; const jsonLdElement = root.querySelector("script#schema\\:music-album"); let artistName = "Unknown Artist"; if (jsonLdElement) { const jsonLd = JSON.parse(jsonLdElement.text); artistName = jsonLd.byArtist[0].name; if (jsonLd && jsonLd.tracks) { jsonLd.tracks.forEach((track) => { tracks.push({ id: track.url.split("/").pop() || "", duration: parseDuration(track.duration || "PT0S"), title: track.name || "Unknown Title", url: track.url || "", thumbnail: thumbnail || "https://music.apple.com/assets/favicon/favicon-180.png", artist: artistName }); }); } } return { id: root.querySelector('meta[name="apple:content_id"]')?.getAttribute("content") || "", title: title || "Unknown Album", artwork: thumbnail || "https://music.apple.com/assets/favicon/favicon-180.png", artist: artistName, url: link, tracks, description: title }; } catch (err) { return null; } } }; // src/AppleMusicExtractor.ts var AppleMusicExtractor = class extends import_discord_player.BaseExtractor { async activate() { this.protocols = ["amsearch", "applemusic"]; const fn = this.options.createStream; if (typeof fn === "function") this._stream = (q, t) => fn(this, q, t); } async deactivate() { this.protocols = []; } async validate(query) { return !isUrl(query) || [appleMusicAlbumRegex, appleMusicPlaylistRegex, appleMusicSongRegex].some((regex) => regex.test(query)); } buildTrack(trackInfo, context, playlist) { const track = new import_discord_player.Track(this.context.player, { author: trackInfo.artist, description: trackInfo.title, duration: trackInfo.duration, thumbnail: trackInfo.thumbnail, title: trackInfo.title, url: trackInfo.url, views: 0, source: "apple_music", requestedBy: context.requestedBy, metadata: { source: trackInfo, bridge: null }, requestMetadata: async () => ({ source: trackInfo, bridge: null }), playlist }); track.extractor = this; return track; } buildPlaylist(data, context, type) { const playlist = new import_discord_player.Playlist(this.context.player, { author: { name: type === "album" ? data.artist : data.description, url: "" }, description: data.description || data.title, id: data.id || "", source: "apple_music", thumbnail: data.artwork, title: data.title, tracks: [], type, url: data.url || "", rawPlaylist: data }); playlist.tracks = data.tracks.map((track) => this.buildTrack(track, context, playlist)); return playlist; } async handle(query, context) { if (appleMusicAlbumRegex.test(query)) { const info = await AppleMusic.getAlbumInfo(query); if (!info) return this.createResponse(); const playlist = this.buildPlaylist(info, context, "album"); return this.createResponse(playlist, playlist.tracks); } if (appleMusicPlaylistRegex.test(query)) { const info = await AppleMusic.getPlaylistInfo(query); if (!info) return this.createResponse(); const playlist = this.buildPlaylist(info, context, "playlist"); return this.createResponse(playlist, playlist.tracks); } if (appleMusicSongRegex.test(query)) { const info = await AppleMusic.getSongInfo(query); if (!info) return this.createResponse(); return this.createResponse(null, [this.buildTrack(info, context)]); } const data = await AppleMusic.search(query); if (!data?.length) return this.createResponse(); return this.createResponse(null, data.map((track) => this.buildTrack(track, context))); } 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; } }; AppleMusicExtractor.identifier = "com.discord-player.applemusicextractor"; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { AppleMusicExtractor });