distube-apple-music
Version:
A DisTube info extractor plugin for supporting Apple Music.
327 lines (325 loc) • 11.2 kB
JavaScript
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
// src/API.ts
import fetch from "isomorphic-unfetch";
import { parse } from "node-html-parser";
function getHTML(link) {
return fetch(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) => 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
import { DisTubeError, InfoExtractorPlugin, Playlist, Song } from "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 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 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 DisTubeError("APPLE_MUSIC_PLUGIN_API_ERROR", "Failed to get data from Apple Music API.");
}
if (!data.type || !SUPPORTED_TYPES.includes(data.type)) {
throw new DisTubeError("APPLE_MUSIC_PLUGIN_NOT_SUPPORTED", "This applemusic link is not supported.");
}
if (data.type === "track") {
return new 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 Playlist(
{
source: "applemusic",
url: data.url,
name: data.title,
id: data.id.toString(),
thumbnail: data.thumbnail,
songs: data.tracks.map(
(song) => new 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 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 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;
export {
AppleMusicPlugin,
src_default as default
};
//# sourceMappingURL=index.mjs.map