aqualink
Version:
An Lavalink client, focused in pure performance and features
148 lines (116 loc) • 4.41 kB
JavaScript
const https = require('https');
const sourceHandlers = {
spotify: fetchSpotifyThumbnail,
youtube: fetchYouTubeThumbnail
};
const YOUTUBE_QUALITIES = ['maxresdefault', 'hqdefault', 'mqdefault', 'default'];
const YOUTUBE_ID_REGEX = /^[a-zA-Z0-9_-]{11}$/;
const SPOTIFY_URI_REGEX = /^https:\/\/open\.spotify\.com\/(track|album|playlist)\/[a-zA-Z0-9]+/;
async function getImageUrl(info) {
if (!info?.sourceName || !info?.uri) return null;
const sourceName = info.sourceName.toLowerCase();
const handler = sourceHandlers[sourceName];
if (!handler) return null;
try {
const param = sourceName === 'spotify' ? info.uri :
sourceName === 'youtube' ? extractYouTubeId(info.uri) : info.uri;
if (!param) return null;
return await handler(param);
} catch (error) {
console.error(`Error fetching ${sourceName} thumbnail:`, error.message);
return null;
}
}
function extractYouTubeId(uri) {
if (!uri) return null;
let id = null;
if (uri.includes('youtube.com/watch?v=')) {
id = uri.split('v=')[1]?.split('&')[0];
} else if (uri.includes('youtu.be/')) {
id = uri.split('youtu.be/')[1]?.split('?')[0];
} else if (uri.includes('youtube.com/embed/')) {
id = uri.split('embed/')[1]?.split('?')[0];
} else if (YOUTUBE_ID_REGEX.test(uri)) {
id = uri;
}
return id && YOUTUBE_ID_REGEX.test(id) ? id : null;
}
async function fetchSpotifyThumbnail(uri) {
if (!SPOTIFY_URI_REGEX.test(uri)) {
throw new Error('Invalid Spotify URI format');
}
const url = `https://open.spotify.com/oembed?url=${encodeURIComponent(uri)}`;
try {
const data = await fetchJson(url);
return data?.thumbnail_url || null;
} catch (error) {
throw new Error(`Spotify fetch failed: ${error.message}`);
}
}
async function fetchYouTubeThumbnail(identifier) {
if (!identifier || !YOUTUBE_ID_REGEX.test(identifier)) {
throw new Error('Invalid YouTube identifier');
}
for (const quality of YOUTUBE_QUALITIES) {
const url = `https://img.youtube.com/vi/${identifier}/${quality}.jpg`;
try {
const exists = await checkImageExists(url);
if (exists) return url;
} catch (error) {
continue;
}
}
return null;
}
function fetchJson(url) {
return new Promise((resolve, reject) => {
const request = https.get(url, (res) => {
if (res.statusCode !== 200) {
res.resume();
return reject(new Error(`HTTP ${res.statusCode}`));
}
const chunks = [];
let totalLength = 0;
res.on('data', chunk => {
chunks.push(chunk);
totalLength += chunk.length;
if (totalLength > 1024 * 1024) {
res.destroy();
reject(new Error('Response too large'));
}
});
res.on('end', () => {
try {
const data = Buffer.concat(chunks, totalLength).toString('utf8');
const json = JSON.parse(data);
resolve(json);
} catch (error) {
reject(new Error(`JSON parse error: ${error.message}`));
}
});
});
request.setTimeout(5000, () => {
request.destroy();
reject(new Error('Request timeout'));
});
request.on('error', (error) => {
reject(new Error(`Request error: ${error.message}`));
});
});
}
function checkImageExists(url) {
return new Promise((resolve) => {
const request = https.request(url, { method: 'HEAD' }, (res) => {
resolve(res.statusCode === 200);
});
request.setTimeout(3000, () => {
request.destroy();
resolve(false);
});
request.on('error', () => resolve(false));
request.end();
});
}
module.exports = {
getImageUrl,
};