distube-apple-music
Version:
A DisTube info extractor plugin for supporting Apple Music.
362 lines (359 loc) • 13 kB
JavaScript
;
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