From f66aa36890abbefff040a0cf7610c72e2bbdf1df Mon Sep 17 00:00:00 2001 From: ld3zian <42660576+ld3z@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:40:58 -0700 Subject: [PATCH 1/4] update custom feed validatity --- package-lock.json | 28 ++++++++++++++++++++++++++++ package.json | 1 + src/pages/rss/[feedId].xml.ts | 26 ++++++++++++++++++++------ src/pages/rss/custom/[slugs].xml.ts | 26 ++++++++++++++++++++------ 4 files changed, 69 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index ced729e..9b39710 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@vercel/analytics": "^1.4.1", "@vercel/kv": "^1.0.1", "astro": "^5.1.6", + "feed": "^4.2.2", "ioredis": "^5.4.2", "tailwindcss": "^3.4.1" } @@ -2970,6 +2971,17 @@ "reusify": "^1.0.4" } }, + "node_modules/feed": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", + "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", + "dependencies": { + "xml-js": "^1.6.11" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -5558,6 +5570,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -7038,6 +7055,17 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, "node_modules/xxhash-wasm": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", diff --git a/package.json b/package.json index 5e529dc..cc44e93 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@vercel/analytics": "^1.4.1", "@vercel/kv": "^1.0.1", "astro": "^5.1.6", + "feed": "^4.2.2", "ioredis": "^5.4.2", "tailwindcss": "^3.4.1" } diff --git a/src/pages/rss/[feedId].xml.ts b/src/pages/rss/[feedId].xml.ts index d589c17..37c1c49 100644 --- a/src/pages/rss/[feedId].xml.ts +++ b/src/pages/rss/[feedId].xml.ts @@ -1,4 +1,4 @@ -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"; @@ -28,14 +28,21 @@ export const GET: APIRoute = async ({ params }) => { return new Response("No chapters found for the provided comics", { status: 404 }); } - return rss({ + 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: "https://github.com/ld3z/manga-rss", + link: "https://github.com/ld3z/manga-rss", + language: lang, + copyright: "All rights reserved", + updated: new Date(), + }); + + chapters.forEach((chapter) => { + feed.addItem({ title: `${chapter.md_comics.title} - Chapter ${chapter.chap}`, + id: `${chapter.md_comics.slug}-${chapter.chap}`, link: `https://comick.io/comic/${chapter.md_comics.slug}`, - pubDate: new Date(chapter.updated_at), description: `
${ @@ -47,6 +54,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..7aafc15 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,29 @@ export const GET: APIRoute = async ({ params, url }) => { return new Response("No chapters found for the provided comics", { status: 404 }); } - return rss({ + 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: "https://github.com/ld3z/manga-rss", + link: "https://github.com/ld3z/manga-rss", + language: lang, + copyright: "All rights reserved", + updated: new Date(), + }); + + chapters.forEach((chapter) => { + feed.addItem({ title: `${chapter.md_comics.title} - Chapter ${chapter.chap}`, + id: `${chapter.md_comics.slug}-${chapter.chap}`, link: `https://comick.io/comic/${chapter.md_comics.slug}`, - pubDate: new Date(chapter.updated_at), 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 From 40552e471c3b6dc3a577dcfe8833a5169577a3f2 Mon Sep 17 00:00:00 2001 From: ld3zian <42660576+ld3z@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:48:05 -0700 Subject: [PATCH 2/4] fix feed? --- src/pages/rss/[feedId].xml.ts | 17 ++++++++++++----- src/pages/rss/custom/[slugs].xml.ts | 15 +++++++++++---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/pages/rss/[feedId].xml.ts b/src/pages/rss/[feedId].xml.ts index 37c1c49..4fcb502 100644 --- a/src/pages/rss/[feedId].xml.ts +++ b/src/pages/rss/[feedId].xml.ts @@ -3,7 +3,7 @@ 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,21 +28,28 @@ export const GET: APIRoute = async ({ params }) => { return new Response("No chapters found for the provided comics", { status: 404 }); } + 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(', ')}`, - id: "https://github.com/ld3z/manga-rss", - link: "https://github.com/ld3z/manga-rss", + id: feedUrl, + link: siteUrl, + feedLinks: { + rss2: feedUrl + }, language: lang, copyright: "All rights reserved", updated: new Date(), }); chapters.forEach((chapter) => { + const chapterUrl = `https://comick.io/comic/${chapter.md_comics.slug}`; feed.addItem({ title: `${chapter.md_comics.title} - Chapter ${chapter.chap}`, - id: `${chapter.md_comics.slug}-${chapter.chap}`, - link: `https://comick.io/comic/${chapter.md_comics.slug}`, + id: `urn:comick:${chapter.md_comics.slug}:${chapter.chap}`, + link: chapterUrl, description: `
${ diff --git a/src/pages/rss/custom/[slugs].xml.ts b/src/pages/rss/custom/[slugs].xml.ts index 7aafc15..dd94d9f 100644 --- a/src/pages/rss/custom/[slugs].xml.ts +++ b/src/pages/rss/custom/[slugs].xml.ts @@ -22,21 +22,28 @@ export const GET: APIRoute = async ({ params, url }) => { return new Response("No chapters found for the provided comics", { status: 404 }); } + 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(', ')}`, - id: "https://github.com/ld3z/manga-rss", - link: "https://github.com/ld3z/manga-rss", + id: feedUrl, + link: siteUrl, + feedLinks: { + rss2: feedUrl + }, language: lang, copyright: "All rights reserved", updated: new Date(), }); chapters.forEach((chapter) => { + const chapterUrl = `https://comick.io/comic/${chapter.md_comics.slug}`; feed.addItem({ title: `${chapter.md_comics.title} - Chapter ${chapter.chap}`, - id: `${chapter.md_comics.slug}-${chapter.chap}`, - link: `https://comick.io/comic/${chapter.md_comics.slug}`, + id: `urn:comick:${chapter.md_comics.slug}:${chapter.chap}`, + link: chapterUrl, description: `New chapter available: Chapter ${chapter.chap}`, date: new Date(chapter.updated_at), }); From 6ca77cf6afa2bdc9f9c4167759ff751c9163dd5a Mon Sep 17 00:00:00 2001 From: ld3zian <42660576+ld3z@users.noreply.github.com> Date: Tue, 28 Jan 2025 18:34:35 -0700 Subject: [PATCH 3/4] fixed the feed --- src/pages/rss/[feedId].xml.ts | 3 ++- src/pages/rss/custom/[slugs].xml.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/rss/[feedId].xml.ts b/src/pages/rss/[feedId].xml.ts index 4fcb502..9429878 100644 --- a/src/pages/rss/[feedId].xml.ts +++ b/src/pages/rss/[feedId].xml.ts @@ -42,13 +42,14 @@ export const GET: APIRoute = async ({ params, request }) => { 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}`, - id: `urn:comick:${chapter.md_comics.slug}:${chapter.chap}`, + id: `${chapter.md_comics.slug}-${chapter.chap}`, link: chapterUrl, description: `
diff --git a/src/pages/rss/custom/[slugs].xml.ts b/src/pages/rss/custom/[slugs].xml.ts index dd94d9f..d29f95c 100644 --- a/src/pages/rss/custom/[slugs].xml.ts +++ b/src/pages/rss/custom/[slugs].xml.ts @@ -36,13 +36,14 @@ export const GET: APIRoute = async ({ params, url }) => { 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}`, - id: `urn:comick:${chapter.md_comics.slug}:${chapter.chap}`, + id: `${chapter.md_comics.slug}-${chapter.chap}`, link: chapterUrl, description: `New chapter available: Chapter ${chapter.chap}`, date: new Date(chapter.updated_at), From 17ceea25d97e34a43cb04044eb5a9a822c9ad7d0 Mon Sep 17 00:00:00 2001 From: ld3zian <42660576+ld3z@users.noreply.github.com> Date: Tue, 28 Jan 2025 18:49:06 -0700 Subject: [PATCH 4/4] better error handling and performance improvements --- src/components/ComicCard.astro | 4 ++- src/config/backgrounds.ts | 7 ++++++ src/layouts/Layout.astro | 11 +++++---- src/lib/api.ts | 27 ++++++++++++++------ src/lib/db.ts | 45 +++++++++++++++++++++++++++++++--- src/lib/types.ts | 16 +++++++----- 6 files changed, 87 insertions(+), 23 deletions(-) diff --git a/src/components/ComicCard.astro b/src/components/ComicCard.astro index 37687b2..f7923b0 100644 --- a/src/components/ComicCard.astro +++ b/src/components/ComicCard.astro @@ -20,6 +20,8 @@ const isNSFW = comic.md_comics.content_rating === 'erotica';
@@ -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