mirror of
https://github.com/ld3z/new-manga.git
synced 2026-11-09 21:23:45 -07:00
Compare commits
5 commits
019f86ad13
...
f84046c66c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f84046c66c | ||
|
|
40da736b63 | ||
|
|
33db20e16b | ||
|
|
8e6ea73460 | ||
|
|
8e7a3af051 |
9 changed files with 189 additions and 30 deletions
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -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.7",
|
"@types/node": "^22.13.9",
|
||||||
"@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.7",
|
"version": "22.13.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
|
||||||
"integrity": "sha512-oU2q+BsQldB9lYxHNp/5aZO+/Bs0Usa74Abo9mAKulz4ahQyXRHK6UVKYIN8KSC8HXwhWSi7b49JnX+txuac0w==",
|
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.20.0"
|
"undici-types": "~6.20.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.7",
|
"@types/node": "^22.13.9",
|
||||||
"@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",
|
||||||
|
|
|
||||||
BIN
public/genshin.webp
Normal file
BIN
public/genshin.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 312 KiB |
BIN
public/meata.webp
Normal file
BIN
public/meata.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/sleepy.webp
Normal file
BIN
public/sleepy.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
|
|
@ -476,17 +476,51 @@ const languages = [
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const response = await fetch(`/api/feed/${feedId}`);
|
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
|
||||||
|
|
||||||
// Update language if provided
|
// Validate response structure
|
||||||
if (lang && langSelect) {
|
if (!data || typeof data !== 'object') {
|
||||||
langSelect.value = lang;
|
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;
|
||||||
|
} else {
|
||||||
|
console.warn(`Feed has unsupported language: ${lang}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new slugs to existing collection
|
// 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
|
// Update UI
|
||||||
updateSlugs();
|
updateSlugs();
|
||||||
|
|
@ -495,7 +529,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!';
|
successDiv.textContent = `Feed imported successfully! Added ${importCount} comics.`;
|
||||||
feedIdInput.parentNode?.appendChild(successDiv);
|
feedIdInput.parentNode?.appendChild(successDiv);
|
||||||
setTimeout(() => successDiv.remove(), 3000);
|
setTimeout(() => successDiv.remove(), 3000);
|
||||||
|
|
||||||
|
|
@ -503,9 +537,11 @@ 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 = '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);
|
feedIdInput.parentNode?.appendChild(errorDiv);
|
||||||
setTimeout(() => errorDiv.remove(), 3000);
|
setTimeout(() => errorDiv.remove(), 5000);
|
||||||
} finally {
|
} finally {
|
||||||
importFeedButton.disabled = false;
|
importFeedButton.disabled = false;
|
||||||
importFeedButton.innerHTML = `
|
importFeedButton.innerHTML = `
|
||||||
|
|
|
||||||
|
|
@ -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!
|
// Add more backgrounds here!
|
||||||
// {
|
// {
|
||||||
// url: 'your-image-url',
|
// url: 'your-image-url',
|
||||||
|
|
|
||||||
|
|
@ -284,19 +284,82 @@ 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> {
|
||||||
const redis = await getRedisClient();
|
let foundMapping: string | null = null;
|
||||||
|
let sourceRedisName: string = "";
|
||||||
|
|
||||||
|
// First try primary
|
||||||
try {
|
try {
|
||||||
const mapping = await redis.get(feedId);
|
// Check primary database if available
|
||||||
if (!mapping) return null;
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await redis.expire(feedId, DEFAULT_CACHE_TTL);
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const data = JSON.parse(mapping);
|
// If we found the mapping in any database, re-synchronize to all other databases
|
||||||
return {
|
if (foundMapping) {
|
||||||
slugs: data.slugs,
|
console.log(`Found mapping in ${sourceRedisName}. Re-synchronizing to all Redis instances...`);
|
||||||
lang: data.lang
|
|
||||||
};
|
// 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) {
|
} 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');
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,43 @@ 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) {
|
||||||
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,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching feed:', error);
|
console.error(`Error fetching feed ${feedId}:`, 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 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue