@@ -50,7 +52,7 @@ const isNSFW = comic.md_comics.content_rating === 'erotica';
-
+
{comic.md_comics.title}
diff --git a/src/config/backgrounds.ts b/src/config/backgrounds.ts
index 329fce9..4179bc8 100644
--- a/src/config/backgrounds.ts
+++ b/src/config/backgrounds.ts
@@ -75,3 +75,10 @@ export function getRandomBackground(): BackgroundImage {
}
return backgrounds[Math.floor(Math.random() * backgrounds.length)];
}
+
+export function preloadBackgrounds() {
+ backgrounds.forEach(bg => {
+ const img = new Image();
+ img.src = bg.url;
+ });
+}
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
index 7c27522..7ce0919 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -147,11 +147,12 @@ const isCreditsPage = Astro.url.pathname === '/credits';
}
};
- // Initialize theme
- const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null;
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
- const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
- updateThemeToggle(initialTheme);
+ // Add theme initialization to prevent flash of wrong theme
+ const themeScript = `
+ const theme = localStorage.getItem('theme') ??
+ (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
+ document.documentElement.setAttribute('data-theme', theme);
+ `;
// Event listeners
settingsButton?.addEventListener('click', () => {
diff --git a/src/lib/api.ts b/src/lib/api.ts
index d566cbe..3d482f1 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -69,30 +69,43 @@ export function getGenreNames(genreIds: number[], genres: Genre[]): string[] {
.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(
language: string = 'en',
contentType: string = 'sfw',
comicType: ComicType = 'all',
page: number = 1
): Promise
{
+ const apiUrl = `${urlBase}?lang=${language}&page=${page}&order=new&accept_erotic_content=${contentType === 'nsfw'}${comicType === 'all' ? '' : `&type=${comicType}`}`;
+
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);
const response = await fetchWithRetry(apiUrl);
const data = await response.json();
if (!Array.isArray(data)) {
- console.error('Unexpected API response format:', data);
- return [];
+ throw new APIError('Invalid API response format', undefined, apiUrl);
}
return data;
} catch (error) {
- console.error('Error fetching comics:', error);
- return [];
+ if (error instanceof APIError) throw error;
+ throw new APIError(
+ 'Failed to fetch comics',
+ error instanceof Response ? error.status : undefined,
+ apiUrl
+ );
}
}
diff --git a/src/lib/db.ts b/src/lib/db.ts
index 84a8deb..ffcc509 100644
--- a/src/lib/db.ts
+++ b/src/lib/db.ts
@@ -3,6 +3,8 @@ import type { FeedMapping } from './types';
const REDIS_URL = import.meta.env.REDIS_URL || process.env.REDIS_URL;
let redis: Redis | null = null;
+let redisRetryCount = 0;
+const MAX_RETRIES = 3;
try {
console.log('Environment check:', {
@@ -23,11 +25,14 @@ try {
console.log(`Retry attempt ${times} with delay ${delay}ms`);
return delay;
},
- maxRetriesPerRequest: 3,
+ maxRetriesPerRequest: 2,
enableReadyCheck: true,
- reconnectOnError(err) {
- console.log('Reconnect on error:', err.message);
- return true;
+ reconnectOnError: (err) => {
+ const targetError = 'READONLY';
+ if (err.message.includes(targetError)) {
+ return true;
+ }
+ return false;
}
});
@@ -55,6 +60,38 @@ try {
}
}
+async function getRedisClient(): Promise {
+ 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 {
if (!redis) {
console.error('Redis client not initialized');
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 69b3f03..2970743 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -6,11 +6,9 @@ export interface Comic {
title: string;
slug: string;
genres: number[];
- md_covers: {
- b2key: string;
- }[];
- content_rating: string;
- comic_type: string;
+ md_covers: ComicCover[];
+ content_rating: 'safe' | 'suggestive' | 'erotica';
+ comic_type: ComicType;
};
}
@@ -24,4 +22,10 @@ export interface FeedMapping {
lang: string;
}
-export type ComicType = 'manga' | 'manhwa' | 'manhua' | 'all';
\ No newline at end of file
+export type ComicType = 'manga' | 'manhwa' | 'manhua' | 'all';
+
+export interface ComicCover {
+ b2key: string;
+ width?: number;
+ height?: number;
+}
\ No newline at end of file
diff --git a/src/pages/rss/[feedId].xml.ts b/src/pages/rss/[feedId].xml.ts
index d589c17..9429878 100644
--- a/src/pages/rss/[feedId].xml.ts
+++ b/src/pages/rss/[feedId].xml.ts
@@ -1,9 +1,9 @@
-import rss from "@astrojs/rss";
+import { Feed } from 'feed';
import { getChaptersForSlugs, isValidLanguage } from "../../lib/api";
import { getFeedMapping } from "../../lib/db";
import type { APIRoute } from "astro";
-export const GET: APIRoute = async ({ params }) => {
+export const GET: APIRoute = async ({ params, request }) => {
const feedId = params.feedId?.replace('.xml', '');
if (!feedId) {
@@ -28,14 +28,29 @@ export const GET: APIRoute = async ({ params }) => {
return new Response("No chapters found for the provided comics", { status: 404 });
}
- return rss({
+ const siteUrl = new URL(request.url).origin;
+ const feedUrl = new URL(request.url).href;
+
+ const feed = new Feed({
title: `ComicK - Custom Feed (${lang.toUpperCase()})`,
description: `Custom RSS feed for selected comics: ${slugs.join(', ')}`,
- site: "https://github.com/ld3z/manga-rss",
- items: chapters.map((chapter) => ({
+ id: feedUrl,
+ link: siteUrl,
+ feedLinks: {
+ rss2: feedUrl
+ },
+ language: lang,
+ copyright: "All rights reserved",
+ updated: new Date(),
+ generator: 'ComicK RSS Generator',
+ });
+
+ chapters.forEach((chapter) => {
+ const chapterUrl = `https://comick.io/comic/${chapter.md_comics.slug}`;
+ feed.addItem({
title: `${chapter.md_comics.title} - Chapter ${chapter.chap}`,
- link: `https://comick.io/comic/${chapter.md_comics.slug}`,
- pubDate: new Date(chapter.updated_at),
+ id: `${chapter.md_comics.slug}-${chapter.chap}`,
+ link: chapterUrl,
description: `
${
@@ -47,6 +62,13 @@ export const GET: APIRoute = async ({ params }) => {
New chapter available: Chapter ${chapter.chap}
`,
- })),
+ date: new Date(chapter.updated_at),
+ });
+ });
+
+ return new Response(feed.rss2(), {
+ headers: {
+ 'Content-Type': 'application/xml; charset=utf-8',
+ },
});
};
\ No newline at end of file
diff --git a/src/pages/rss/custom/[slugs].xml.ts b/src/pages/rss/custom/[slugs].xml.ts
index f5bb501..d29f95c 100644
--- a/src/pages/rss/custom/[slugs].xml.ts
+++ b/src/pages/rss/custom/[slugs].xml.ts
@@ -1,4 +1,4 @@
-import rss from "@astrojs/rss";
+import { Feed } from 'feed';
import { getChaptersForSlugs, isValidLanguage } from "../../../lib/api";
import type { APIRoute } from "astro";
@@ -22,15 +22,37 @@ export const GET: APIRoute = async ({ params, url }) => {
return new Response("No chapters found for the provided comics", { status: 404 });
}
- return rss({
+ const siteUrl = new URL(url).origin;
+ const feedUrl = new URL(url).href;
+
+ const feed = new Feed({
title: `ComicK - Custom Feed (${lang.toUpperCase()})`,
description: `Custom RSS feed for selected comics: ${slugs.join(', ')}`,
- site: "https://github.com/ld3z/manga-rss",
- items: chapters.map((chapter) => ({
+ id: feedUrl,
+ link: siteUrl,
+ feedLinks: {
+ rss2: feedUrl
+ },
+ language: lang,
+ copyright: "All rights reserved",
+ updated: new Date(),
+ generator: 'ComicK RSS Generator',
+ });
+
+ chapters.forEach((chapter) => {
+ const chapterUrl = `https://comick.io/comic/${chapter.md_comics.slug}`;
+ feed.addItem({
title: `${chapter.md_comics.title} - Chapter ${chapter.chap}`,
- link: `https://comick.io/comic/${chapter.md_comics.slug}`,
- pubDate: new Date(chapter.updated_at),
+ id: `${chapter.md_comics.slug}-${chapter.chap}`,
+ link: chapterUrl,
description: `New chapter available: Chapter ${chapter.chap}`,
- })),
+ date: new Date(chapter.updated_at),
+ });
+ });
+
+ return new Response(feed.rss2(), {
+ headers: {
+ 'Content-Type': 'application/xml; charset=utf-8',
+ },
});
};
\ No newline at end of file