mirror of
https://github.com/ld3z/new-manga.git
synced 2026-11-09 20:53:45 -07:00
Compare commits
4 commits
c81866e53b
...
17ceea25d9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17ceea25d9 | ||
|
|
6ca77cf6af | ||
|
|
40552e471c | ||
|
|
f66aa36890 |
10 changed files with 175 additions and 38 deletions
28
package-lock.json
generated
28
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ const isNSFW = comic.md_comics.content_rating === 'erotica';
|
|||
<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"
|
||||
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">
|
||||
<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="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}
|
||||
</h3>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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<Comic[]> {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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> {
|
||||
if (!redis) {
|
||||
console.error('Redis client not initialized');
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
export type ComicType = 'manga' | 'manhwa' | 'manhua' | 'all';
|
||||
|
||||
export interface ComicCover {
|
||||
b2key: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
|
@ -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: `
|
||||
<div>
|
||||
${
|
||||
|
|
@ -47,6 +62,13 @@ export const GET: APIRoute = async ({ params }) => {
|
|||
<p>New chapter available: Chapter ${chapter.chap}</p>
|
||||
</div>
|
||||
`,
|
||||
})),
|
||||
date: new Date(chapter.updated_at),
|
||||
});
|
||||
});
|
||||
|
||||
return new Response(feed.rss2(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/xml; charset=utf-8',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue