Files
blog/scripts/generate_circle_data.js
T
2026-03-01 09:27:18 +08:00

154 lines
5.2 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const Parser = require('rss-parser');
const LINK_LITE_PATH = path.join(__dirname, '../themes/Ying/static/json/link_lite.json');
const OUTPUT_PATH = path.join(__dirname, '../themes/Ying/static/json/friend_circle_data.json');
const MAX_POSTS_PER_FRIEND = 5;
const MAX_TOTAL_POSTS = 100; // Limit total output size
const parser = new Parser({
timeout: 10000, // 10s timeout
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'
}
});
async function fetchFeed(url) {
try {
const feed = await parser.parseURL(url);
return feed;
} catch (error) {
throw error;
}
}
async function main() {
console.log('Starting Friend Circle generation...');
if (!fs.existsSync(LINK_LITE_PATH)) {
console.error('link_lite.json not found!');
process.exit(1);
}
const data = JSON.parse(fs.readFileSync(LINK_LITE_PATH, 'utf8'));
// data.friends is an array of arrays: [name, url, avatar, rss?]
let allPosts = [];
// Revert to processing one by one or small batches with sequential candidate checks
// to ensure reliability over speed.
const friends = data.friends;
// Using a simple loop to process friends to ensure stability
// We can run a few in parallel, but let's keep candidate checking sequential
const CONCURRENCY = 5; // Conservative concurrency
for (let i = 0; i < friends.length; i += CONCURRENCY) {
const batch = friends.slice(i, i + CONCURRENCY);
const promises = batch.map(async (friend) => {
const name = friend[0];
const blogUrl = friend[1];
const avatar = friend[2];
let rssUrl = friend.length >= 4 ? friend[3] : null;
// Heuristics for RSS URL
const candidates = [];
if (rssUrl) {
candidates.push(rssUrl);
} else {
const cleanUrl = blogUrl.replace(/\/$/, '');
candidates.push(`${cleanUrl}/atom.xml`);
candidates.push(`${cleanUrl}/rss.xml`);
candidates.push(`${cleanUrl}/feed`);
candidates.push(`${cleanUrl}/feed/`);
candidates.push(`${cleanUrl}/index.xml`);
}
let feed = null;
// Sequential candidate check (Reliable)
for (const url of candidates) {
try {
feed = await fetchFeed(url);
// console.log(`[${name}] Fetched RSS successfully: ${url}`);
break; // Found one, stop checking
} catch (e) {
// Continue to next candidate
}
}
if (!feed) {
console.log(`[${name}] No valid RSS found.`);
return;
}
// Extract posts
if (!feed.items) return;
const posts = feed.items.slice(0, MAX_POSTS_PER_FRIEND).map(item => {
let img = null;
// Try to find an image in content
const content = item['content:encoded'] || item.content || item.description || '';
const imgMatch = content.match(/<img[^>]+src=['"]([^'"]+)['"]/i);
if (imgMatch) {
img = imgMatch[1];
} else if (item.enclosure && item.enclosure.url && item.enclosure.type && item.enclosure.type.startsWith('image')) {
img = item.enclosure.url;
}
// Get a short snippet
let snippet = '';
if (content) {
snippet = content.replace(/<[^>]+>/g, '');
snippet = snippet.replace(/\s+/g, ' ').trim();
if (snippet.length > 120) {
snippet = snippet.substring(0, 120) + '...';
}
}
return {
title: item.title,
link: item.link,
date: item.isoDate || item.pubDate,
author: name,
avatar: avatar,
blogUrl: blogUrl,
image: img,
description: snippet
};
});
allPosts = allPosts.concat(posts);
});
await Promise.all(promises);
process.stdout.write(`Processed ${Math.min(i + CONCURRENCY, friends.length)}/${friends.length} friends...\r`);
}
console.log('\nSorting and saving...');
// Sort by date desc
allPosts.sort((a, b) => {
return new Date(b.date) - new Date(a.date);
});
// Limit total
const finalPosts = allPosts.slice(0, MAX_TOTAL_POSTS);
const output = {
updated: new Date().toISOString(),
posts: finalPosts
};
fs.writeFileSync(OUTPUT_PATH, JSON.stringify(output, null, 2));
console.log(`Saved ${finalPosts.length} posts to ${OUTPUT_PATH}`);
process.exit(0);
}
main();