inital release

This commit is contained in:
obvtiger 2025-04-30 19:01:10 +02:00
commit 1d708c14cf
26 changed files with 34335 additions and 0 deletions

132
routes/avatar.js Normal file
View file

@ -0,0 +1,132 @@
const axios = require('axios');
const cheerio = require('cheerio');
const avatarCache = new Map();
const pendingFetches = new Map();
const CACHE_TTL = 24 * 60 * 60 * 1000;
async function fetchAvatarImage(channelId) {
try {
const channelUrl = `https://www.youtube.com/${channelId.startsWith('UC') ? 'channel/' : ''}${channelId}`;
console.log("loading" + channelUrl);
const htmlResponse = await axios.get(channelUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
});
const $ = cheerio.load(htmlResponse.data);
const imageUrl = $('meta[property="og:image"]').attr('content');
if (!imageUrl) {
throw new Error('Profile picture URL not found in page');
}
const imageResponse = await axios.get(imageUrl, {
responseType: 'arraybuffer',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
});
const buffer = Buffer.from(imageResponse.data);
const contentType = imageResponse.headers['content-type'] || 'image/jpeg';
avatarCache.set(channelId, {
buffer,
contentType,
timestamp: Date.now()
});
return { buffer, contentType };
} catch (error) {
throw new Error(`Failed to fetch avatar: ${error.message}`);
}
}
function avatarRouteHandler(app) {
app.get("/api/avatar/:channelId.png", async (req, res) => {
try {
let channelId = req.params.channelId;
if (!channelId) {
return res.status(400).json({ error: "Invalid channel ID" });
}
if (!channelId.startsWith('UC') && !channelId.startsWith('@')) {
channelId = `@${channelId}`;
}
const cached = avatarCache.get(channelId);
if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) {
console.log(`Cache hit for ${channelId}`);
res.set('Content-Type', cached.contentType);
return res.send(cached.buffer);
}
if (pendingFetches.has(channelId)) {
console.log(`Awaiting pending fetch for ${channelId}`);
const { buffer, contentType } = await pendingFetches.get(channelId);
res.set('Content-Type', contentType);
return res.send(buffer);
}
console.log(`Fetching avatar for ${channelId}`);
const fetchPromise = fetchAvatarImage(channelId);
pendingFetches.set(channelId, fetchPromise);
try {
const { buffer, contentType } = await fetchPromise;
res.set('Content-Type', contentType);
res.send(buffer);
} finally {
pendingFetches.delete(channelId);
}
} catch (err) {
console.error(`Error fetching avatar: ${err.message} ${err.stack}`);
res.status(500).json({ error: "Error fetching avatar" });
}
});
app.get("/api/avatar/channel/:channelId.png", async (req, res) => {
try {
let channelId = req.params.channelId;
if (!channelId && !channelId.startsWith('UC')) {
return res.status(400).json({ error: "Invalid channel ID" });
}
const cached = avatarCache.get(channelId);
if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) {
console.log(`Cache hit for ${channelId}`);
res.set('Content-Type', cached.contentType);
return res.send(cached.buffer);
}
if (pendingFetches.has(channelId)) {
console.log(`Awaiting pending fetch for ${channelId}`);
const { buffer, contentType } = await pendingFetches.get(channelId);
res.set('Content-Type', contentType);
return res.send(buffer);
}
console.log(`Fetching avatar for ${channelId}`);
const fetchPromise = fetchAvatarImage(channelId);
pendingFetches.set(channelId, fetchPromise);
try {
const { buffer, contentType } = await fetchPromise;
res.set('Content-Type', contentType);
res.send(buffer);
} finally {
pendingFetches.delete(channelId);
}
} catch (err) {
console.error(`Error fetching avatar: ${err.message} ${err.stack}`);
res.status(500).json({ error: "Error fetching avatar" });
}
});
}
module.exports = avatarRouteHandler;