Compare commits

..

No commits in common. "f84046c66c3b9dd5b6cba1807756cca076d721df" and "019f86ad1300bd19e44f28bda8b35581e30130db" have entirely different histories.

9 changed files with 30 additions and 189 deletions

8
package-lock.json generated
View file

@ -12,7 +12,7 @@
"@astrojs/tailwind": "^6.0.0", "@astrojs/tailwind": "^6.0.0",
"@astrojs/vercel": "^8.1.0", "@astrojs/vercel": "^8.1.0",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@types/node": "^22.13.9", "@types/node": "^22.13.7",
"@vercel/analytics": "^1.5.0", "@vercel/analytics": "^1.5.0",
"@vercel/kv": "^3.0.0", "@vercel/kv": "^3.0.0",
"astro": "^5.4.1", "astro": "^5.4.1",
@ -1465,9 +1465,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.13.9", "version": "22.13.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.7.tgz",
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", "integrity": "sha512-oU2q+BsQldB9lYxHNp/5aZO+/Bs0Usa74Abo9mAKulz4ahQyXRHK6UVKYIN8KSC8HXwhWSi7b49JnX+txuac0w==",
"dependencies": { "dependencies": {
"undici-types": "~6.20.0" "undici-types": "~6.20.0"
} }

View file

@ -15,7 +15,7 @@
"@astrojs/tailwind": "^6.0.0", "@astrojs/tailwind": "^6.0.0",
"@astrojs/vercel": "^8.1.0", "@astrojs/vercel": "^8.1.0",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@types/node": "^22.13.9", "@types/node": "^22.13.7",
"@vercel/analytics": "^1.5.0", "@vercel/analytics": "^1.5.0",
"@vercel/kv": "^3.0.0", "@vercel/kv": "^3.0.0",
"astro": "^5.4.1", "astro": "^5.4.1",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

View file

@ -476,51 +476,17 @@ const languages = [
`; `;
const response = await fetch(`/api/feed/${feedId}`); const response = await fetch(`/api/feed/${feedId}`);
if (!response.ok) { if (!response.ok) throw new Error('Failed to fetch feed');
const errorText = await response.text();
console.error(`Feed import failed with status ${response.status}: ${errorText}`);
throw new Error(`Failed to fetch feed: ${response.status} ${response.statusText}`);
}
const data = await response.json(); const { slugs: feedSlugs, lang } = await response.json();
console.log('Feed import data:', data); // Diagnostic log
// Validate response structure // Update language if provided
if (!data || typeof data !== 'object') { if (lang && langSelect) {
throw new Error('Invalid feed data format: not an object');
}
const feedSlugs = data.slugs;
const lang = data.lang;
// Validate slugs array
if (!feedSlugs || !Array.isArray(feedSlugs)) {
throw new Error('Invalid feed data: missing or invalid slugs array');
}
// Validate language
if (!lang || typeof lang !== 'string') {
console.warn('Feed has missing or invalid language, using default');
} else if (langSelect) {
// Only update language if it's valid
const validLanguages = Array.from(langSelect.options).map(opt => opt.value);
if (validLanguages.includes(lang)) {
langSelect.value = lang; langSelect.value = lang;
} else {
console.warn(`Feed has unsupported language: ${lang}`);
}
} }
// Add new slugs to existing collection // Add new slugs to existing collection
let importCount = 0; feedSlugs.forEach((slug: string) => slugs.add(slug));
feedSlugs.forEach((slug: string) => {
if (typeof slug === 'string' && slug.trim()) {
slugs.add(slug.trim());
importCount++;
} else {
console.warn('Skipping invalid slug in feed:', slug);
}
});
// Update UI // Update UI
updateSlugs(); updateSlugs();
@ -529,7 +495,7 @@ const languages = [
// Show success message // Show success message
const successDiv = document.createElement('div'); const successDiv = document.createElement('div');
successDiv.className = 'text-sm text-green-500 mt-1'; successDiv.className = 'text-sm text-green-500 mt-1';
successDiv.textContent = `Feed imported successfully! Added ${importCount} comics.`; successDiv.textContent = 'Feed imported successfully!';
feedIdInput.parentNode?.appendChild(successDiv); feedIdInput.parentNode?.appendChild(successDiv);
setTimeout(() => successDiv.remove(), 3000); setTimeout(() => successDiv.remove(), 3000);
@ -537,11 +503,9 @@ const languages = [
console.error('Failed to import feed:', error); console.error('Failed to import feed:', error);
const errorDiv = document.createElement('div'); const errorDiv = document.createElement('div');
errorDiv.className = 'text-sm text-red-500 mt-1'; errorDiv.className = 'text-sm text-red-500 mt-1';
errorDiv.textContent = error instanceof Error errorDiv.textContent = 'Failed to import feed. Please check the feed ID and try again.';
? `Import failed: ${error.message}`
: 'Failed to import feed. Please check the feed ID and try again.';
feedIdInput.parentNode?.appendChild(errorDiv); feedIdInput.parentNode?.appendChild(errorDiv);
setTimeout(() => errorDiv.remove(), 5000); setTimeout(() => errorDiv.remove(), 3000);
} finally { } finally {
importFeedButton.disabled = false; importFeedButton.disabled = false;
importFeedButton.innerHTML = ` importFeedButton.innerHTML = `

View file

@ -74,43 +74,6 @@ export const backgrounds: BackgroundImage[] = [
}, },
], ],
}, },
{
url: "/sleepy.webp",
title: "???",
artist: "soaphisoap",
source: "https://safebooru.org/index.php?page=post&s=view&id=5418793",
socials: [
{
name: "Twitter",
link: "https://x.com/soaphisoap",
},
],
},
{
url: "/genshin.webp",
title: "???",
artist: "charlieinoni",
source: "https://safebooru.org/index.php?page=post&s=view&id=5531630",
socials: [
{
name: "Twitter",
link: "https://x.com/charlieinoni",
},
],
},
{
url: "/meata.webp",
title: "???",
artist: "???",
source:
"https://i.pximg.net/img-original/img/2025/03/03/15/13/24/127822472_p0.png",
socials: [
{
name: "Twitter",
link: "https://x.com/",
},
],
},
// Add more backgrounds here! // Add more backgrounds here!
// { // {
// url: 'your-image-url', // url: 'your-image-url',

View file

@ -284,82 +284,19 @@ export async function checkCacheStatus(slugs: string[], lang: string): Promise<s
} }
export async function getFeedMapping(feedId: string): Promise<FeedMapping | null> { export async function getFeedMapping(feedId: string): Promise<FeedMapping | null> {
let foundMapping: string | null = null; const redis = await getRedisClient();
let sourceRedisName: string = "";
// First try primary
try { try {
// Check primary database if available const mapping = await redis.get(feedId);
if (primaryClient?.status === 'ready') { if (!mapping) return null;
try {
await primaryClient.ping();
const mapping = await primaryClient.get(feedId);
if (mapping) {
await primaryClient.expire(feedId, DEFAULT_CACHE_TTL);
foundMapping = mapping;
sourceRedisName = "Primary";
}
} catch (error) {
console.warn('Primary Redis connection failed while retrieving mapping:', error);
// Fall through to try replicas
}
}
// If not found in primary, check replicas await redis.expire(feedId, DEFAULT_CACHE_TTL);
if (!foundMapping) {
for (const [url, client] of replicaClients.entries()) {
if (client.status === 'ready') {
try {
await client.ping();
console.log(`Trying to get key from ${getRedisName(url)}`);
const mapping = await client.get(feedId);
if (mapping) {
await client.expire(feedId, DEFAULT_CACHE_TTL);
foundMapping = mapping;
sourceRedisName = getRedisName(url);
break; // Exit the loop once we find the mapping
}
} catch (error) {
console.warn(`${getRedisName(url)} connection failed while retrieving mapping:`, error);
// Continue to next replica
}
}
}
}
// If we found the mapping in any database, re-synchronize to all other databases const data = JSON.parse(mapping);
if (foundMapping) { return {
console.log(`Found mapping in ${sourceRedisName}. Re-synchronizing to all Redis instances...`);
// Parse data for returning
const data = JSON.parse(foundMapping);
const result = {
slugs: data.slugs, slugs: data.slugs,
lang: data.lang lang: data.lang
}; };
// Fire-and-forget re-sync to all databases
(async () => {
try {
await writeToAllRedis(async (redis) => {
await redis.set(
feedId,
foundMapping!,
'EX',
DEFAULT_CACHE_TTL
);
});
console.log(`Successfully re-synchronized mapping for ${feedId} to all Redis instances`);
} catch (syncError) {
console.error('Error during Redis re-synchronization:', syncError);
}
})();
return result;
}
// If we get here, the key wasn't found in any database
return null;
} catch (error) { } catch (error) {
console.error('Error retrieving feed mapping:', error); console.error('Error retrieving feed mapping:', error);
throw new Error('Failed to retrieve feed mapping'); throw new Error('Failed to retrieve feed mapping');

View file

@ -9,43 +9,20 @@ export const GET: APIRoute = async ({ params }) => {
} }
try { try {
console.log(`Fetching feed mapping for ID: ${feedId}`);
const mapping = await getFeedMapping(feedId); const mapping = await getFeedMapping(feedId);
if (!mapping) { if (!mapping) {
console.error(`Feed not found for ID: ${feedId}`); return new Response("Feed not found", { status: 404 });
return new Response(`Feed with ID "${feedId}" not found`, { status: 404 });
} }
// Validate mapping structure before returning return new Response(JSON.stringify(mapping), {
if (!Array.isArray(mapping.slugs)) {
console.error(`Invalid slugs in feed mapping for ID: ${feedId}`, mapping);
return new Response(`Feed data corruption: invalid slugs format`, {
status: 500
});
}
// Ensure consistent response format
const response = {
slugs: mapping.slugs.filter(slug => typeof slug === 'string' && slug.trim()),
lang: typeof mapping.lang === 'string' ? mapping.lang : 'en'
};
console.log(`Successfully retrieved feed mapping for ID: ${feedId}`, response);
return new Response(JSON.stringify(response), {
status: 200, status: 200,
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}); });
} catch (error) { } catch (error) {
console.error(`Error fetching feed ${feedId}:`, error); console.error('Error fetching feed:', error);
return new Response("Internal server error", { status: 500 });
const errorMessage = error instanceof Error
? `Internal server error: ${error.message}`
: "Internal server error";
return new Response(errorMessage, { status: 500 });
} }
}; };