@mjba/lyrics
Version:
A TypeScript library for fetching song lyrics from Musixmatch
668 lines (664 loc) • 22.2 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 __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 index_exports = {};
__export(index_exports, {
LyricsClient: () => LyricsClient,
lyricsClient: () => lyricsClient
});
module.exports = __toCommonJS(index_exports);
var import_node_path2 = require("path");
// src/utils.ts
var import_promises = require("fs/promises");
var import_node_os = require("os");
var import_node_path = require("path");
var import_fetch_cookie = __toESM(require("fetch-cookie"), 1);
var import_node_fetch = __toESM(require("node-fetch"), 1);
function isNode() {
return typeof process !== "undefined" && process.versions !== void 0 && typeof process.versions.node === "string";
}
function isBun() {
return typeof globalThis.Bun !== "undefined";
}
function isDeno() {
return typeof globalThis.Deno !== "undefined";
}
function isBrowser() {
return typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
}
async function mkdir(path) {
if (isNode() || isBun()) {
try {
await (0, import_promises.mkdir)(path, { recursive: true });
} catch (error) {
const nodeError = error;
if (nodeError.code !== "EEXIST") {
throw error;
}
}
} else if (isDeno()) {
try {
const deno = globalThis.Deno;
if (deno) {
await deno.mkdir(path, { recursive: true });
}
} catch (error) {
const deno = globalThis.Deno;
if (deno && !(error instanceof deno.errors.AlreadyExists)) {
throw error;
}
}
}
}
async function writeFile(path, data) {
if (isNode() || isBun()) {
await (0, import_promises.writeFile)(path, data, "utf-8");
} else if (isDeno()) {
const deno = globalThis.Deno;
if (deno) {
await deno.writeTextFile(path, data);
}
}
}
async function readFile(path) {
if (isNode() || isBun()) {
return await (0, import_promises.readFile)(path, "utf-8");
} else if (isDeno()) {
const deno = globalThis.Deno;
if (deno) {
return await deno.readTextFile(path);
}
}
throw new Error("File system operations not supported in browser");
}
function getCachePath(appName) {
if (isNode() || isBun()) {
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".cache", appName);
} else if (isDeno()) {
const deno = globalThis.Deno;
if (deno) {
const homeDir = deno.env.get("HOME") || deno.env.get("USERPROFILE") || "/tmp";
return `${homeDir}/.cache/${appName}`;
}
}
return appName;
}
var fetchInstance = null;
async function getFetch() {
if (fetchInstance) {
return fetchInstance;
}
if (isBrowser()) {
fetchInstance = globalThis.fetch;
return fetchInstance;
}
if (isDeno()) {
fetchInstance = globalThis.fetch;
return fetchInstance;
}
if (isNode() || isBun()) {
try {
fetchInstance = (0, import_fetch_cookie.default)(import_node_fetch.default);
return fetchInstance;
} catch (_error) {
try {
fetchInstance = import_node_fetch.default;
return fetchInstance;
} catch (_fallbackError) {
throw new Error(
"node-fetch is required for Node.js/Bun environments. Please install it: npm install node-fetch"
);
}
}
}
throw new Error("No fetch implementation available");
}
// src/index.ts
var LyricsClient = class {
constructor() {
this.ROOT_URL = "https://apic-desktop.musixmatch.com/ws/1.1/";
this.token = null;
}
parseSubtitleToSyncedLyrics(subtitleBody) {
const lines = subtitleBody.split("\n");
const timestampMap = /* @__PURE__ */ new Map();
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine) continue;
const match = trimmedLine.match(/\[(\d{2}):(\d{2})\.(\d{2})\]\s*(.+)/);
if (match?.[1] && match[2] && match[3] && match[4]) {
const [, minutes, seconds, hundredths, text] = match;
const timestampKey = `${minutes}:${seconds}.${hundredths}`;
if (!timestampMap.has(timestampKey)) {
timestampMap.set(timestampKey, []);
}
const existingTexts = timestampMap.get(timestampKey);
if (existingTexts) {
existingTexts.push(text.trim());
}
}
}
const syncedLyrics = [];
for (const [timestampKey, textParts] of timestampMap) {
const match = timestampKey.match(/(\d{2}):(\d{2})\.(\d{2})/);
if (!match || !match[1] || !match[2] || !match[3]) continue;
const [, minutes, seconds, hundredths] = match;
const minutesNum = parseInt(minutes, 10);
const secondsNum = parseInt(seconds, 10);
const hundredthsNum = parseInt(hundredths, 10);
const msNum = hundredthsNum * 10;
const totalSeconds = minutesNum * 60 + secondsNum + hundredthsNum / 100;
const combinedText = textParts.filter((text) => text.length > 0).join(" ").trim();
if (combinedText) {
syncedLyrics.push({
text: combinedText,
time: {
total: totalSeconds,
minutes: minutesNum,
seconds: secondsNum,
ms: msNum
}
});
}
}
syncedLyrics.sort((a, b) => a.time.total - b.time.total);
return syncedLyrics;
}
parseRichSyncToSyncedLyrics(richsyncBody) {
try {
const richsyncData = JSON.parse(richsyncBody);
const timestampMap = /* @__PURE__ */ new Map();
if (Array.isArray(richsyncData)) {
for (const item of richsyncData) {
if (item.ts && item.l && Array.isArray(item.l)) {
const startTime = parseFloat(item.ts);
for (const lyricItem of item.l) {
if (lyricItem.c) {
if (!timestampMap.has(startTime)) {
timestampMap.set(startTime, []);
}
const existingTexts = timestampMap.get(startTime);
if (existingTexts) {
existingTexts.push(lyricItem.c);
}
}
}
}
}
}
const syncedLyrics = [];
for (const [startTime, textParts] of timestampMap) {
const minutes = Math.floor(startTime / 60);
const seconds = Math.floor(startTime % 60);
const ms = Math.floor(startTime % 1 * 1e3);
const combinedText = textParts.filter((text) => text.trim().length > 0).join(" ").trim();
if (combinedText) {
syncedLyrics.push({
text: combinedText,
time: {
total: startTime,
minutes,
seconds,
ms
}
});
}
}
syncedLyrics.sort((a, b) => a.time.total - b.time.total);
return syncedLyrics;
} catch {
return [];
}
}
async get(action, query = []) {
if (action !== "token.get" && this.token === null) {
await this.getToken();
}
query.push(["app_id", "web-desktop-app-v1.0"]);
if (this.token !== null) {
query.push(["usertoken", this.token]);
}
const timestamp = Date.now().toString();
query.push(["t", timestamp]);
const url = this.ROOT_URL + action;
const params = new URLSearchParams(query);
try {
const fetch = await getFetch();
const response = await fetch(`${url}?${params.toString()}`, {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/115.0.0.0 Safari/537.36",
Accept: "application/json, text/javascript, */*; q=0.01",
Referer: "https://www.musixmatch.com/",
Origin: "https://www.musixmatch.com"
}
});
return response;
} catch (error) {
console.error("Error making Musixmatch request:", error);
throw error;
}
}
async getToken() {
if (isNode() || isBun()) {
const tokenPath = (0, import_node_path2.join)(
getCachePath("syncedlyrics"),
"musixmatch_token.json"
);
const currentTime = Math.floor(Date.now() / 1e3);
try {
const tokenData = await readFile(tokenPath);
const cachedTokenData = JSON.parse(tokenData);
const cachedToken = cachedTokenData.token;
const expirationTime = cachedTokenData.expiration_time;
if (cachedToken && expirationTime && currentTime < expirationTime) {
this.token = cachedToken;
return;
}
} catch {
console.warn("No valid cached token found, fetching a new one.");
}
try {
const response = await this.get("token.get", [["user_language", "en"]]);
const data = await response.json();
if (data.message.header.status_code === 401) {
await new Promise((resolve) => setTimeout(resolve, 1e4));
return this.getToken();
}
const newToken = data.message.body.user_token;
const expirationTime = currentTime + 600;
this.token = newToken;
const tokenData = {
token: newToken,
expiration_time: expirationTime
};
try {
const tokenDir = (0, import_node_path2.dirname)(tokenPath);
await mkdir(tokenDir);
await writeFile(tokenPath, JSON.stringify(tokenData));
} catch (writeError) {
console.warn("Could not cache token:", writeError);
}
} catch (error) {
console.error("Error getting Musixmatch token:", error);
throw error;
}
} else {
try {
const response = await this.get("token.get", [["user_language", "en"]]);
const data = await response.json();
if (data.message.header.status_code === 401) {
await new Promise((resolve) => setTimeout(resolve, 1e4));
return this.getToken();
}
this.token = data.message.body.user_token;
} catch (error) {
console.error("Error getting Musixmatch token:", error);
throw error;
}
}
}
/**
* Get synced lyrics with timestamps by ISRC code
* @param isrc - International Standard Recording Code
* @returns Promise<LyricsResponse>
*/
async getSyncedLyricsByISRC(isrc) {
try {
const trackResponse = await this.get("track.get", [["track_isrc", isrc]]);
const trackData = await trackResponse.json();
if (trackData.message.header.status_code !== 200) {
return {
success: false,
error: `Track not found for ISRC: ${isrc}`
};
}
const track = trackData.message.body.track;
const trackId = track.track_id;
try {
const richsyncResponse = await this.get("track.richsync.get", [
["track_id", trackId.toString()]
]);
const richsyncData = await richsyncResponse.json();
if (richsyncData.message.header.status_code === 200) {
const richsync = richsyncData.message.body.richsync;
const syncedLyrics = this.parseRichSyncToSyncedLyrics(
richsync.richsync_body
);
if (syncedLyrics.length > 0) {
return {
success: true,
syncedLyrics,
hasTimestamps: true,
songInfo: {
title: track.track_name,
artist: track.artist_name,
album: track.album_name,
duration: track.track_length
}
};
}
}
} catch {
}
try {
const subtitleResponse = await this.get("track.subtitle.get", [
["track_id", trackId.toString()]
]);
const subtitleData = await subtitleResponse.json();
if (subtitleData.message.header.status_code === 200) {
const subtitle = subtitleData.message.body.subtitle;
const syncedLyrics = this.parseSubtitleToSyncedLyrics(
subtitle.subtitle_body
);
if (syncedLyrics.length > 0) {
return {
success: true,
syncedLyrics,
hasTimestamps: true,
songInfo: {
title: track.track_name,
artist: track.artist_name,
album: track.album_name,
duration: track.track_length
}
};
}
}
} catch {
}
const lyricsResponse = await this.get("track.lyrics.get", [
["track_id", trackId.toString()]
]);
const lyricsData = await lyricsResponse.json();
if (lyricsData.message.header.status_code !== 200) {
return {
success: false,
error: "No lyrics found for this track"
};
}
const lyrics = lyricsData.message.body.lyrics;
return {
success: true,
lyrics: lyrics.lyrics_body,
hasTimestamps: false,
songInfo: {
title: track.track_name,
artist: track.artist_name,
album: track.album_name,
duration: track.track_length
}
};
} catch (error) {
console.error("Error fetching synced lyrics by ISRC:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error occurred"
};
}
}
/**
* Search for track and get synced lyrics with timestamps
* @param query - Search query (artist and track name)
* @returns Promise<LyricsResponse>
*/
async searchAndGetSyncedLyrics(query) {
try {
const searchResponse = await this.get("track.search", [
["q", query],
["page_size", "1"],
["s_track_rating", "desc"]
]);
const searchData = await searchResponse.json();
if (searchData.message.header.status_code !== 200 || !searchData.message.body.track_list || searchData.message.body.track_list.length === 0) {
return {
success: false,
error: `No tracks found for query: ${query}`
};
}
const trackList = searchData.message.body.track_list;
const firstTrack = trackList[0];
if (!firstTrack) {
return {
success: false,
error: "No tracks found in search results"
};
}
const track = firstTrack.track;
const trackId = track.track_id;
try {
const richsyncResponse = await this.get("track.richsync.get", [
["track_id", trackId.toString()]
]);
const richsyncData = await richsyncResponse.json();
if (richsyncData.message.header.status_code === 200) {
const richsync = richsyncData.message.body.richsync;
const syncedLyrics = this.parseRichSyncToSyncedLyrics(
richsync.richsync_body
);
if (syncedLyrics.length > 0) {
return {
success: true,
syncedLyrics,
hasTimestamps: true,
songInfo: {
title: track.track_name,
artist: track.artist_name,
album: track.album_name,
duration: track.track_length
}
};
}
}
} catch {
}
try {
const subtitleResponse = await this.get("track.subtitle.get", [
["track_id", trackId.toString()]
]);
const subtitleData = await subtitleResponse.json();
if (subtitleData.message.header.status_code === 200) {
const subtitle = subtitleData.message.body.subtitle;
const syncedLyrics = this.parseSubtitleToSyncedLyrics(
subtitle.subtitle_body
);
if (syncedLyrics.length > 0) {
return {
success: true,
syncedLyrics,
hasTimestamps: true,
songInfo: {
title: track.track_name,
artist: track.artist_name,
album: track.album_name,
duration: track.track_length
}
};
}
}
} catch {
}
const lyricsResponse = await this.get("track.lyrics.get", [
["track_id", trackId.toString()]
]);
const lyricsData = await lyricsResponse.json();
if (lyricsData.message.header.status_code !== 200) {
return {
success: false,
error: "No lyrics found for this track"
};
}
const lyrics = lyricsData.message.body.lyrics;
return {
success: true,
lyrics: lyrics.lyrics_body,
hasTimestamps: false,
songInfo: {
title: track.track_name,
artist: track.artist_name,
album: track.album_name,
duration: track.track_length
}
};
} catch (error) {
console.error("Error searching and fetching synced lyrics:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error occurred"
};
}
}
/**
* Get lyrics by ISRC code
* @param isrc - International Standard Recording Code
* @returns Promise<LyricsResponse>
*/
async getLyricsByISRC(isrc) {
try {
const trackResponse = await this.get("track.get", [["track_isrc", isrc]]);
const trackData = await trackResponse.json();
if (trackData.message.header.status_code !== 200) {
return {
success: false,
error: `Track not found for ISRC: ${isrc}`
};
}
const track = trackData.message.body.track;
const trackId = track.track_id;
const lyricsResponse = await this.get("track.lyrics.get", [
["track_id", trackId.toString()]
]);
const lyricsData = await lyricsResponse.json();
if (lyricsData.message.header.status_code !== 200) {
return {
success: false,
error: "Lyrics not found for this track"
};
}
const lyrics = lyricsData.message.body.lyrics;
return {
success: true,
lyrics: lyrics.lyrics_body,
songInfo: {
title: track.track_name,
artist: track.artist_name,
album: track.album_name,
duration: track.track_length
}
};
} catch (error) {
console.error("Error fetching lyrics by ISRC:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error occurred"
};
}
}
/**
* Search for track and get lyrics
* @param query - Search query (artist and track name)
* @returns Promise<LyricsResponse>
*/
async searchAndGetLyrics(query) {
try {
const searchResponse = await this.get("track.search", [
["q", query],
["page_size", "1"],
["s_track_rating", "desc"]
]);
const searchData = await searchResponse.json();
if (searchData.message.header.status_code !== 200 || !searchData.message.body.track_list || searchData.message.body.track_list.length === 0) {
return {
success: false,
error: `No tracks found for query: ${query}`
};
}
const trackList = searchData.message.body.track_list;
const firstTrack = trackList[0];
if (!firstTrack) {
return {
success: false,
error: "No tracks found in search results"
};
}
const track = firstTrack.track;
const trackId = track.track_id;
const lyricsResponse = await this.get("track.lyrics.get", [
["track_id", trackId.toString()]
]);
const lyricsData = await lyricsResponse.json();
if (lyricsData.message.header.status_code !== 200) {
return {
success: false,
error: "Lyrics not found for this track"
};
}
const lyrics = lyricsData.message.body.lyrics;
return {
success: true,
lyrics: lyrics.lyrics_body,
songInfo: {
title: track.track_name,
artist: track.artist_name,
album: track.album_name,
duration: track.track_length
}
};
} catch (error) {
console.error("Error searching and fetching lyrics:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error occurred"
};
}
}
/**
* Get track information by ISRC without lyrics
* @param isrc - International Standard Recording Code
* @returns Promise<Track>
*/
async getTrackByISRC(isrc) {
try {
const response = await this.get("track.get", [["track_isrc", isrc]]);
const data = await response.json();
if (data.message.header.status_code !== 200) {
throw new Error(`Track not found for ISRC: ${isrc}`);
}
return data.message.body.track;
} catch (error) {
console.error("Error fetching track by ISRC:", error);
throw error;
}
}
};
var lyricsClient = new LyricsClient();
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
LyricsClient,
lyricsClient
});