UNPKG

distube-apple-music

Version:

A DisTube info extractor plugin for supporting Apple Music.

362 lines (359 loc) 13 kB
"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, { AppleMusicPlugin: () => AppleMusicPlugin, default: () => src_default }); module.exports = __toCommonJS(src_exports); // src/API.ts var import_isomorphic_unfetch = __toESM(require("isomorphic-unfetch")); var import_node_html_parser = require("node-html-parser"); function getHTML(link) { return (0, import_isomorphic_unfetch.default)(link, { headers: { "User-Agent": ( // eslint-disable-next-line max-len "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" ) } }).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"); var AppleMusic = class _AppleMusic { static { __name(this, "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" }, type: "track" })); } catch { return []; } } static 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 song = { id: contentId, 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" }, type: "track" }; return song; } catch { return null; } } static async getSongInfo(link) { 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, 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" }, type: "track" }; } catch { return this.getSongInfoFallback(res, name, id, link); } } static async getPlaylistInfo(link) { 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, type: "playlist", tracks: ( // eslint-disable-next-line pl.ogSongs?.map((m) => { const song = m.attributes; return { id: m.id, 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" }, type: "track" }; }) || [] ) }; } catch { return null; } } static async getAlbumInfo(link) { 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, type: "album", tracks: ( // eslint-disable-next-line pl.ogSongs?.map((m) => { const song = m.attributes; return { id: m.id, 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" }, type: "track" }; }) || [] ) }; } catch { return null; } } }; // src/index.ts var import_distube = require("distube"); var SUPPORTED_TYPES = ["album", "playlist", "track"]; var SONG_REGEX = /^https?:\/\/music\.apple\.com\/.+?\/(song|album)\/.+?(\/.+?\?i=|\/)([0-9]+)$/; var PLAYLIST_REGEX = /^https?:\/\/music\.apple\.com\/.+?\/playlist\/.+\/pl\.(u-|pm-)?[a-zA-Z0-9]+$/; var ALBUM_REGEX = /^https?:\/\/music\.apple\.com\/.+?\/album\/.+\/([0-9]+)$/; var parseURL = /* @__PURE__ */ __name((url) => { let type; if (SONG_REGEX.test(url)) { type = "track"; } else if (PLAYLIST_REGEX.test(url)) { type = "playlist"; } else if (ALBUM_REGEX.test(url)) { type = "album"; } return { type }; }, "parseURL"); var AppleMusicPlugin = class extends import_distube.InfoExtractorPlugin { static { __name(this, "AppleMusicPlugin"); } validate(url) { if (typeof url !== "string" || !url.includes("music.apple.com")) return false; const { type } = parseURL(url); if (!type || !SUPPORTED_TYPES.includes(type)) return false; return true; } async resolve(url, { member, metadata }) { const { type } = parseURL(url); if (!type) throw new import_distube.DisTubeError("APPLE_MUSIC_PLUGIN_INVALID_URL", `Invalid Apple Music url: ${url}`); const data = type === "track" ? await AppleMusic.getSongInfo(url) : type === "album" ? await AppleMusic.getAlbumInfo(url) : await AppleMusic.getPlaylistInfo(url); if (!data) { throw new import_distube.DisTubeError("APPLE_MUSIC_PLUGIN_API_ERROR", "Failed to get data from Apple Music API."); } if (!data.type || !SUPPORTED_TYPES.includes(data.type)) { throw new import_distube.DisTubeError("APPLE_MUSIC_PLUGIN_NOT_SUPPORTED", "This applemusic link is not supported."); } if (data.type === "track") { return new import_distube.Song( { plugin: this, source: "applemusic", playFromSource: false, id: data.id.toString(), url: data.url, name: data.title, uploader: { name: data.artist.name, url: data.artist.name }, thumbnail: data.thumbnail }, { member, metadata } ); } return new import_distube.Playlist( { source: "applemusic", url: data.url, name: data.title, id: data.id.toString(), thumbnail: data.thumbnail, songs: data.tracks.map( (song) => new import_distube.Song( { plugin: this, source: "applemusic", playFromSource: false, url: song.url, name: song.title, uploader: { name: song.artist.name }, thumbnail: song.thumbnail }, { member, metadata } ) ) }, { member, metadata } ); } createSearchQuery(song) { return `${song.name} ${song.uploader.name}`; } async getRelatedSongs(song) { if (!song || !song.url) { throw new import_distube.DisTubeError("APPLE_MUSIC_PLUGIN_INVALID_SONG", "Cannot get related songs from invalid song."); } const related = await AppleMusic.search(`${song.uploader.name || song.name}`); const unique = related.filter((r) => r.url !== song.url); return unique.map( (t) => new import_distube.Song( { plugin: this, source: "applemusic", playFromSource: false, url: t.url, name: t.title, uploader: { name: t.artist.name }, thumbnail: t.thumbnail }, {} ) ); } }; var src_default = AppleMusicPlugin; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { AppleMusicPlugin }); //# sourceMappingURL=index.js.map