diff --git a/package-lock.json b/package-lock.json index 340d133..853b281 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@astrojs/tailwind": "^6.0.0", "@astrojs/vercel": "^8.1.0", "@floating-ui/dom": "^1.6.13", - "@types/node": "^22.13.7", + "@types/node": "^22.13.9", "@vercel/analytics": "^1.5.0", "@vercel/kv": "^3.0.0", "astro": "^5.4.1", @@ -1465,9 +1465,9 @@ } }, "node_modules/@types/node": { - "version": "22.13.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.7.tgz", - "integrity": "sha512-oU2q+BsQldB9lYxHNp/5aZO+/Bs0Usa74Abo9mAKulz4ahQyXRHK6UVKYIN8KSC8HXwhWSi7b49JnX+txuac0w==", + "version": "22.13.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", "dependencies": { "undici-types": "~6.20.0" } diff --git a/package.json b/package.json index 04cbc25..de31832 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@astrojs/tailwind": "^6.0.0", "@astrojs/vercel": "^8.1.0", "@floating-ui/dom": "^1.6.13", - "@types/node": "^22.13.7", + "@types/node": "^22.13.9", "@vercel/analytics": "^1.5.0", "@vercel/kv": "^3.0.0", "astro": "^5.4.1", diff --git a/public/genshin.webp b/public/genshin.webp new file mode 100644 index 0000000..b93365a Binary files /dev/null and b/public/genshin.webp differ diff --git a/public/meata.webp b/public/meata.webp new file mode 100644 index 0000000..08a2e74 Binary files /dev/null and b/public/meata.webp differ diff --git a/public/sleepy.webp b/public/sleepy.webp new file mode 100644 index 0000000..494c25a Binary files /dev/null and b/public/sleepy.webp differ diff --git a/src/components/SlugInput.astro b/src/components/SlugInput.astro index 3a5b5d3..c20b255 100644 --- a/src/components/SlugInput.astro +++ b/src/components/SlugInput.astro @@ -476,17 +476,51 @@ const languages = [ `; const response = await fetch(`/api/feed/${feedId}`); - if (!response.ok) throw new Error('Failed to fetch feed'); + if (!response.ok) { + 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 { slugs: feedSlugs, lang } = await response.json(); + const data = await response.json(); + console.log('Feed import data:', data); // Diagnostic log + + // Validate response structure + if (!data || typeof data !== 'object') { + throw new Error('Invalid feed data format: not an object'); + } - // Update language if provided - if (lang && langSelect) { - langSelect.value = lang; + 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; + } else { + console.warn(`Feed has unsupported language: ${lang}`); + } } // Add new slugs to existing collection - feedSlugs.forEach((slug: string) => slugs.add(slug)); + let importCount = 0; + 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 updateSlugs(); @@ -495,7 +529,7 @@ const languages = [ // Show success message const successDiv = document.createElement('div'); successDiv.className = 'text-sm text-green-500 mt-1'; - successDiv.textContent = 'Feed imported successfully!'; + successDiv.textContent = `Feed imported successfully! Added ${importCount} comics.`; feedIdInput.parentNode?.appendChild(successDiv); setTimeout(() => successDiv.remove(), 3000); @@ -503,9 +537,11 @@ const languages = [ console.error('Failed to import feed:', error); const errorDiv = document.createElement('div'); errorDiv.className = 'text-sm text-red-500 mt-1'; - errorDiv.textContent = 'Failed to import feed. Please check the feed ID and try again.'; + errorDiv.textContent = error instanceof Error + ? `Import failed: ${error.message}` + : 'Failed to import feed. Please check the feed ID and try again.'; feedIdInput.parentNode?.appendChild(errorDiv); - setTimeout(() => errorDiv.remove(), 3000); + setTimeout(() => errorDiv.remove(), 5000); } finally { importFeedButton.disabled = false; importFeedButton.innerHTML = ` diff --git a/src/config/backgrounds.ts b/src/config/backgrounds.ts index 38d2851..e85074f 100644 --- a/src/config/backgrounds.ts +++ b/src/config/backgrounds.ts @@ -74,6 +74,43 @@ 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! // { // url: 'your-image-url', @@ -105,4 +142,4 @@ export function preloadBackgrounds() { const img = new Image(); img.src = bg.url; }); -} \ No newline at end of file +} diff --git a/src/lib/db.ts b/src/lib/db.ts index bb34a83..3b4b04b 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -284,19 +284,82 @@ export async function checkCacheStatus(slugs: string[], lang: string): Promise { - const redis = await getRedisClient(); + let foundMapping: string | null = null; + let sourceRedisName: string = ""; + // First try primary try { - const mapping = await redis.get(feedId); - if (!mapping) return null; - - await redis.expire(feedId, DEFAULT_CACHE_TTL); - - const data = JSON.parse(mapping); - return { - slugs: data.slugs, - lang: data.lang - }; + // Check primary database if available + if (primaryClient?.status === 'ready') { + 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 + 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 + if (foundMapping) { + 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, + 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) { console.error('Error retrieving feed mapping:', error); throw new Error('Failed to retrieve feed mapping'); diff --git a/src/pages/api/feed/[feedId].ts b/src/pages/api/feed/[feedId].ts index 2fe7c8f..bf7a29d 100644 --- a/src/pages/api/feed/[feedId].ts +++ b/src/pages/api/feed/[feedId].ts @@ -9,20 +9,43 @@ export const GET: APIRoute = async ({ params }) => { } try { + console.log(`Fetching feed mapping for ID: ${feedId}`); const mapping = await getFeedMapping(feedId); if (!mapping) { - return new Response("Feed not found", { status: 404 }); + console.error(`Feed not found for ID: ${feedId}`); + return new Response(`Feed with ID "${feedId}" not found`, { status: 404 }); } - return new Response(JSON.stringify(mapping), { + // Validate mapping structure before returning + 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, headers: { 'Content-Type': 'application/json' } }); } catch (error) { - console.error('Error fetching feed:', error); - return new Response("Internal server error", { status: 500 }); + console.error(`Error fetching feed ${feedId}:`, error); + + const errorMessage = error instanceof Error + ? `Internal server error: ${error.message}` + : "Internal server error"; + + return new Response(errorMessage, { status: 500 }); } }; \ No newline at end of file