mirror of
https://github.com/ld3z/new-manga.git
synced 2026-11-09 20:13:44 -07:00
better error handling and performance improvements
This commit is contained in:
parent
6ca77cf6af
commit
17ceea25d9
6 changed files with 87 additions and 23 deletions
|
|
@ -20,6 +20,8 @@ const isNSFW = comic.md_comics.content_rating === 'erotica';
|
||||||
<div
|
<div
|
||||||
class="bg-secondary rounded-xl overflow-hidden shadow-lg h-full flex flex-col max-w-[280px] mx-auto border border-hover/30 hover:border-accent/50 transition-all duration-300"
|
class="bg-secondary rounded-xl overflow-hidden shadow-lg h-full flex flex-col max-w-[280px] mx-auto border border-hover/30 hover:border-accent/50 transition-all duration-300"
|
||||||
data-genres={JSON.stringify(comic.md_comics.genres)}
|
data-genres={JSON.stringify(comic.md_comics.genres)}
|
||||||
|
role="article"
|
||||||
|
aria-labelledby={`comic-title-${comic.id}`}
|
||||||
>
|
>
|
||||||
<a href={`https://comick.io/comic/${comic.md_comics.slug}`} class="flex flex-col h-full group relative">
|
<a href={`https://comick.io/comic/${comic.md_comics.slug}`} class="flex flex-col h-full group relative">
|
||||||
<div class="relative overflow-hidden aspect-[2/3] rounded-t-xl">
|
<div class="relative overflow-hidden aspect-[2/3] rounded-t-xl">
|
||||||
|
|
@ -50,7 +52,7 @@ const isNSFW = comic.md_comics.content_rating === 'erotica';
|
||||||
|
|
||||||
<div class="p-3 sm:p-4 md:p-5 flex-grow flex flex-col bg-secondary relative rounded-b-xl">
|
<div class="p-3 sm:p-4 md:p-5 flex-grow flex flex-col bg-secondary relative rounded-b-xl">
|
||||||
<div class="absolute -top-6 sm:-top-8 left-2 right-2 sm:left-3 sm:right-3 bg-secondary/95 backdrop-blur-[12px] p-2 sm:p-3 rounded-lg shadow-lg border border-hover/30">
|
<div class="absolute -top-6 sm:-top-8 left-2 right-2 sm:left-3 sm:right-3 bg-secondary/95 backdrop-blur-[12px] p-2 sm:p-3 rounded-lg shadow-lg border border-hover/30">
|
||||||
<h3 class="text-base sm:text-lg font-bold line-clamp-1 text-center text-primary drop-shadow-[0_1px_1px_rgba(0,0,0,0.5)]">
|
<h3 id={`comic-title-${comic.id}`} class="text-base sm:text-lg font-bold line-clamp-1 text-center text-primary drop-shadow-[0_1px_1px_rgba(0,0,0,0.5)]">
|
||||||
{comic.md_comics.title}
|
{comic.md_comics.title}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -75,3 +75,10 @@ export function getRandomBackground(): BackgroundImage {
|
||||||
}
|
}
|
||||||
return backgrounds[Math.floor(Math.random() * backgrounds.length)];
|
return backgrounds[Math.floor(Math.random() * backgrounds.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function preloadBackgrounds() {
|
||||||
|
backgrounds.forEach(bg => {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = bg.url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,11 +147,12 @@ const isCreditsPage = Astro.url.pathname === '/credits';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize theme
|
// Add theme initialization to prevent flash of wrong theme
|
||||||
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null;
|
const themeScript = `
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
const theme = localStorage.getItem('theme') ??
|
||||||
const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
|
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||||
updateThemeToggle(initialTheme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
`;
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
settingsButton?.addEventListener('click', () => {
|
settingsButton?.addEventListener('click', () => {
|
||||||
|
|
|
||||||
|
|
@ -69,30 +69,43 @@ export function getGenreNames(genreIds: number[], genres: Genre[]): string[] {
|
||||||
.filter(name => name !== '');
|
.filter(name => name !== '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class APIError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public statusCode?: number,
|
||||||
|
public endpoint?: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'APIError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchComics(
|
export async function fetchComics(
|
||||||
language: string = 'en',
|
language: string = 'en',
|
||||||
contentType: string = 'sfw',
|
contentType: string = 'sfw',
|
||||||
comicType: ComicType = 'all',
|
comicType: ComicType = 'all',
|
||||||
page: number = 1
|
page: number = 1
|
||||||
): Promise<Comic[]> {
|
): Promise<Comic[]> {
|
||||||
|
const apiUrl = `${urlBase}?lang=${language}&page=${page}&order=new&accept_erotic_content=${contentType === 'nsfw'}${comicType === 'all' ? '' : `&type=${comicType}`}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const typeParam = comicType === 'all' ? '' : `&type=${comicType}`;
|
|
||||||
const apiUrl = `${urlBase}?lang=${language}&page=${page}&order=new&accept_erotic_content=${contentType === 'nsfw'}${typeParam}`;
|
|
||||||
|
|
||||||
console.log('Fetching comics from:', apiUrl);
|
console.log('Fetching comics from:', apiUrl);
|
||||||
|
|
||||||
const response = await fetchWithRetry(apiUrl);
|
const response = await fetchWithRetry(apiUrl);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!Array.isArray(data)) {
|
if (!Array.isArray(data)) {
|
||||||
console.error('Unexpected API response format:', data);
|
throw new APIError('Invalid API response format', undefined, apiUrl);
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching comics:', error);
|
if (error instanceof APIError) throw error;
|
||||||
return [];
|
throw new APIError(
|
||||||
|
'Failed to fetch comics',
|
||||||
|
error instanceof Response ? error.status : undefined,
|
||||||
|
apiUrl
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import type { FeedMapping } from './types';
|
||||||
|
|
||||||
const REDIS_URL = import.meta.env.REDIS_URL || process.env.REDIS_URL;
|
const REDIS_URL = import.meta.env.REDIS_URL || process.env.REDIS_URL;
|
||||||
let redis: Redis | null = null;
|
let redis: Redis | null = null;
|
||||||
|
let redisRetryCount = 0;
|
||||||
|
const MAX_RETRIES = 3;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Environment check:', {
|
console.log('Environment check:', {
|
||||||
|
|
@ -23,11 +25,14 @@ try {
|
||||||
console.log(`Retry attempt ${times} with delay ${delay}ms`);
|
console.log(`Retry attempt ${times} with delay ${delay}ms`);
|
||||||
return delay;
|
return delay;
|
||||||
},
|
},
|
||||||
maxRetriesPerRequest: 3,
|
maxRetriesPerRequest: 2,
|
||||||
enableReadyCheck: true,
|
enableReadyCheck: true,
|
||||||
reconnectOnError(err) {
|
reconnectOnError: (err) => {
|
||||||
console.log('Reconnect on error:', err.message);
|
const targetError = 'READONLY';
|
||||||
return true;
|
if (err.message.includes(targetError)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -55,6 +60,38 @@ try {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getRedisClient(): Promise<Redis> {
|
||||||
|
if (redis?.status === 'ready') return redis;
|
||||||
|
|
||||||
|
if (redisRetryCount >= MAX_RETRIES) {
|
||||||
|
throw new Error('Failed to connect to Redis after multiple attempts');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
redis = new Redis(REDIS_URL, {
|
||||||
|
retryStrategy(times) {
|
||||||
|
const delay = Math.min(times * 50, 2000);
|
||||||
|
console.log(`Retry attempt ${times} with delay ${delay}ms`);
|
||||||
|
return delay;
|
||||||
|
},
|
||||||
|
maxRetriesPerRequest: 2,
|
||||||
|
enableReadyCheck: true,
|
||||||
|
reconnectOnError: (err) => {
|
||||||
|
const targetError = 'READONLY';
|
||||||
|
if (err.message.includes(targetError)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
redisRetryCount = 0;
|
||||||
|
return redis;
|
||||||
|
} catch (error) {
|
||||||
|
redisRetryCount++;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function storeFeedMapping(feedId: string, slugs: string[], lang: string): Promise<void> {
|
export async function storeFeedMapping(feedId: string, slugs: string[], lang: string): Promise<void> {
|
||||||
if (!redis) {
|
if (!redis) {
|
||||||
console.error('Redis client not initialized');
|
console.error('Redis client not initialized');
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,9 @@ export interface Comic {
|
||||||
title: string;
|
title: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
genres: number[];
|
genres: number[];
|
||||||
md_covers: {
|
md_covers: ComicCover[];
|
||||||
b2key: string;
|
content_rating: 'safe' | 'suggestive' | 'erotica';
|
||||||
}[];
|
comic_type: ComicType;
|
||||||
content_rating: string;
|
|
||||||
comic_type: string;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,4 +22,10 @@ export interface FeedMapping {
|
||||||
lang: string;
|
lang: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComicType = 'manga' | 'manhwa' | 'manhua' | 'all';
|
export type ComicType = 'manga' | 'manhwa' | 'manhua' | 'all';
|
||||||
|
|
||||||
|
export interface ComicCover {
|
||||||
|
b2key: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue