2026-01-21 23:18:35 +08:00
|
|
|
|
|
|
|
|
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({
|
2026-02-01 13:54:49 +08:00
|
|
|
timeout: 10000, // 10s timeout
|
2026-01-21 23:18:35 +08:00
|
|
|
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) {
|
2026-02-01 13:25:16 +08:00
|
|
|
throw error;
|
2026-01-21 23:18:35 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 = [];
|
|
|
|
|
|
2026-02-01 13:54:49 +08:00
|
|
|
// Revert to processing one by one or small batches with sequential candidate checks
|
|
|
|
|
// to ensure reliability over speed.
|
2026-01-21 23:18:35 +08:00
|
|
|
const friends = data.friends;
|
|
|
|
|
|
2026-02-01 13:54:49 +08:00
|
|
|
// 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);
|
2026-01-21 23:18:35 +08:00
|
|
|
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;
|
2026-02-01 13:54:49 +08:00
|
|
|
|
|
|
|
|
// 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.`);
|
2026-01-21 23:18:35 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract posts
|
2026-02-01 13:54:49 +08:00
|
|
|
if (!feed.items) return;
|
|
|
|
|
|
2026-01-21 23:18:35 +08:00
|
|
|
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,
|
2026-02-01 13:54:49 +08:00
|
|
|
date: item.isoDate || item.pubDate,
|
2026-01-21 23:18:35 +08:00
|
|
|
author: name,
|
|
|
|
|
avatar: avatar,
|
|
|
|
|
blogUrl: blogUrl,
|
|
|
|
|
image: img,
|
|
|
|
|
description: snippet
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
allPosts = allPosts.concat(posts);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await Promise.all(promises);
|
2026-02-01 13:54:49 +08:00
|
|
|
process.stdout.write(`Processed ${Math.min(i + CONCURRENCY, friends.length)}/${friends.length} friends...\r`);
|
2026-01-21 23:18:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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}`);
|
2026-03-01 09:27:18 +08:00
|
|
|
process.exit(0);
|
2026-01-21 23:18:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main();
|