mirror of
https://github.com/ld3z/new-manga.git
synced 2026-11-09 20:43:45 -07:00
add specific feeds for specifc manga
This commit is contained in:
parent
0277da93e5
commit
082db594dd
6 changed files with 397 additions and 3 deletions
163
src/components/SlugInput.astro
Normal file
163
src/components/SlugInput.astro
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
currentSlugs?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { currentSlugs = [] } = Astro.props;
|
||||||
|
|
||||||
|
import { availableLanguages, languageMap } from '../lib/api';
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<label for="slug-input" class="text-sm font-medium text-primary">
|
||||||
|
Add Comic Slug
|
||||||
|
<span class="text-primary/60">(e.g., "01-one-piece" from https://comick.io/comic/01-one-piece)</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="slug-input"
|
||||||
|
placeholder="Enter comic slug..."
|
||||||
|
class="flex-1 px-4 py-2 rounded-md bg-secondary text-primary border border-hover focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent transition-colors"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
id="add-slug"
|
||||||
|
class="px-4 py-2 bg-accent text-secondary hover:bg-accent-light rounded-md transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<label for="lang-select" class="text-sm font-medium text-primary">
|
||||||
|
Select Language
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="lang-select"
|
||||||
|
class="px-4 py-2 rounded-md bg-secondary text-primary border border-hover focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent transition-colors"
|
||||||
|
>
|
||||||
|
{availableLanguages.map(lang => (
|
||||||
|
<option value={lang}>
|
||||||
|
{languageMap[lang as keyof typeof languageMap]}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="slug-list" class="space-y-2">
|
||||||
|
{currentSlugs.map(slug => (
|
||||||
|
<div class="flex items-center justify-between px-4 py-2 bg-secondary rounded-md group" data-slug={slug}>
|
||||||
|
<span class="text-primary">{slug}</span>
|
||||||
|
<button
|
||||||
|
class="text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
data-remove={slug}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-4">
|
||||||
|
<a
|
||||||
|
id="generate-rss"
|
||||||
|
href="#"
|
||||||
|
class="inline-flex items-center gap-2 px-4 py-2 bg-accent text-secondary hover:bg-accent-light rounded-md transition-colors font-medium"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-5 h-5"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M4 11a9 9 0 0 1 9 9"></path>
|
||||||
|
<path d="M4 4a16 16 0 0 1 16 16"></path>
|
||||||
|
<circle cx="5" cy="19" r="1"></circle>
|
||||||
|
</svg>
|
||||||
|
Generate RSS Feed
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const slugInput = document.getElementById('slug-input') as HTMLInputElement;
|
||||||
|
const addButton = document.getElementById('add-slug');
|
||||||
|
const slugList = document.getElementById('slug-list');
|
||||||
|
const generateRSSLink = document.getElementById('generate-rss');
|
||||||
|
const langSelect = document.getElementById('lang-select') as HTMLSelectElement;
|
||||||
|
|
||||||
|
// Load saved slugs from localStorage
|
||||||
|
let slugs = new Set(JSON.parse(localStorage.getItem('comic-slugs') || '[]'));
|
||||||
|
|
||||||
|
function updateSlugs() {
|
||||||
|
localStorage.setItem('comic-slugs', JSON.stringify([...slugs]));
|
||||||
|
updateRSSLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRSSLink() {
|
||||||
|
const slugString = [...slugs].join(',');
|
||||||
|
const lang = langSelect?.value || 'en';
|
||||||
|
generateRSSLink!.setAttribute('href', `/rss/custom/${slugString}.xml?lang=${lang}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSlugList() {
|
||||||
|
if (!slugList) return;
|
||||||
|
|
||||||
|
slugList.innerHTML = '';
|
||||||
|
[...slugs].forEach(slug => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'flex items-center justify-between px-4 py-2 bg-secondary rounded-md group';
|
||||||
|
div.dataset.slug = slug;
|
||||||
|
div.innerHTML = `
|
||||||
|
<span class="text-primary">${slug}</span>
|
||||||
|
<button
|
||||||
|
class="text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
data-remove="${slug}"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
slugList.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSlug(slug: string) {
|
||||||
|
const sanitizedSlug = slug.trim().toLowerCase();
|
||||||
|
if (sanitizedSlug && !slugs.has(sanitizedSlug)) {
|
||||||
|
slugs.add(sanitizedSlug);
|
||||||
|
updateSlugs();
|
||||||
|
renderSlugList();
|
||||||
|
}
|
||||||
|
slugInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
addButton?.addEventListener('click', () => addSlug(slugInput.value));
|
||||||
|
|
||||||
|
slugInput?.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
addSlug(slugInput.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
slugList?.addEventListener('click', (e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const slug = target.dataset.remove;
|
||||||
|
if (slug) {
|
||||||
|
slugs.delete(slug);
|
||||||
|
updateSlugs();
|
||||||
|
renderSlugList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
langSelect?.addEventListener('change', updateRSSLink);
|
||||||
|
|
||||||
|
// Initialize RSS link and render initial list
|
||||||
|
updateRSSLink();
|
||||||
|
renderSlugList();
|
||||||
|
</script>
|
||||||
153
src/lib/api.ts
153
src/lib/api.ts
|
|
@ -100,3 +100,156 @@ export function getLanguageName(code: string): string {
|
||||||
export const availableLanguages = Array.from(languages);
|
export const availableLanguages = Array.from(languages);
|
||||||
export const availableContentTypes = Array.from(contentTypes);
|
export const availableContentTypes = Array.from(contentTypes);
|
||||||
export { languageMap };
|
export { languageMap };
|
||||||
|
|
||||||
|
interface ComicDetail {
|
||||||
|
comic: {
|
||||||
|
hid: string;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChapterDetail {
|
||||||
|
id: string;
|
||||||
|
chap: string;
|
||||||
|
title: string;
|
||||||
|
updated_at: string;
|
||||||
|
md_comics: {
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChapterParams {
|
||||||
|
limit?: number;
|
||||||
|
lang?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getComicBySlug(slug: string): Promise<ComicDetail | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetchWithRetry(`https://api.comick.fun/comic/${slug}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data?.comic?.hid) {
|
||||||
|
console.error(`Invalid comic data for slug ${slug}:`, data);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching comic ${slug}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getChaptersByHid(
|
||||||
|
hid: string,
|
||||||
|
slug: string,
|
||||||
|
params: ChapterParams = { limit: 15, lang: 'en' }
|
||||||
|
): Promise<ChapterDetail[]> {
|
||||||
|
try {
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
limit: params.limit?.toString() || '15',
|
||||||
|
lang: params.lang || 'en',
|
||||||
|
ordering: '-created_at'
|
||||||
|
});
|
||||||
|
|
||||||
|
const comicResponse = await fetchWithRetry(
|
||||||
|
`https://api.comick.fun/comic/${hid}`
|
||||||
|
);
|
||||||
|
const comicData = await comicResponse.json();
|
||||||
|
const comicTitle = comicData?.comic?.title || 'Unknown Comic';
|
||||||
|
|
||||||
|
const response = await fetchWithRetry(
|
||||||
|
`https://api.comick.fun/comic/${hid}/chapters?${queryParams}`
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data?.chapters || !Array.isArray(data.chapters)) {
|
||||||
|
console.error(`Invalid chapters data for hid ${hid}:`, data);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Processing chapters for comic:', comicTitle);
|
||||||
|
|
||||||
|
const chapters = data.chapters
|
||||||
|
.filter(chapter => {
|
||||||
|
const isValid = chapter &&
|
||||||
|
chapter.chap &&
|
||||||
|
chapter.hid &&
|
||||||
|
(chapter.publish_at || chapter.created_at || chapter.updated_at);
|
||||||
|
return isValid;
|
||||||
|
})
|
||||||
|
.map(chapter => ({
|
||||||
|
id: chapter.hid,
|
||||||
|
chap: chapter.chap,
|
||||||
|
title: chapter.title || `Chapter ${chapter.chap}`,
|
||||||
|
updated_at: chapter.publish_at || chapter.created_at || chapter.updated_at,
|
||||||
|
md_comics: {
|
||||||
|
title: comicTitle,
|
||||||
|
slug: slug
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(`Successfully processed ${chapters.length} chapters for ${comicTitle}`);
|
||||||
|
return chapters;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching chapters for hid ${hid}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getChaptersForSlugs(
|
||||||
|
slugs: string[],
|
||||||
|
lang: string = 'en'
|
||||||
|
): Promise<ChapterDetail[]> {
|
||||||
|
console.log(`Fetching chapters for slugs:`, slugs, `in language: ${lang}`);
|
||||||
|
const chapters: ChapterDetail[] = [];
|
||||||
|
|
||||||
|
for (const slug of slugs) {
|
||||||
|
console.log(`Processing slug: ${slug}`);
|
||||||
|
const comicDetail = await getComicBySlug(slug);
|
||||||
|
|
||||||
|
if (!comicDetail) {
|
||||||
|
console.error(`Failed to get comic details for slug: ${slug}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!comicDetail.comic?.hid) {
|
||||||
|
console.error(`No HID found for slug ${slug}`, comicDetail);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found HID ${comicDetail.comic.hid} for slug ${slug}`);
|
||||||
|
const comicChapters = await getChaptersByHid(
|
||||||
|
comicDetail.comic.hid,
|
||||||
|
slug,
|
||||||
|
{
|
||||||
|
limit: 15,
|
||||||
|
lang: lang
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(`Found ${comicChapters.length} chapters for ${slug}`);
|
||||||
|
|
||||||
|
if (comicChapters.length > 0) {
|
||||||
|
chapters.push(...comicChapters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by chapter number (descending) first, then by date
|
||||||
|
const sortedChapters = chapters.sort((a, b) => {
|
||||||
|
// Convert chapter numbers to floats for proper numerical sorting
|
||||||
|
const chapA = parseFloat(a.chap);
|
||||||
|
const chapB = parseFloat(b.chap);
|
||||||
|
|
||||||
|
if (chapB !== chapA) {
|
||||||
|
return chapB - chapA; // Sort by chapter number first
|
||||||
|
}
|
||||||
|
|
||||||
|
// If chapters are the same, sort by date
|
||||||
|
return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Total chapters found: ${sortedChapters.length}`);
|
||||||
|
return sortedChapters;
|
||||||
|
}
|
||||||
|
|
@ -47,7 +47,27 @@ const description = `Latest ${type!.toUpperCase()} comics in ${getLanguageName(l
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8">
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
<LanguageSelector currentLang={lang!} currentType={type!} />
|
<LanguageSelector currentLang={lang!} currentType={type!} />
|
||||||
|
<a
|
||||||
|
href="/custom"
|
||||||
|
class="inline-flex items-center gap-2 px-4 py-2 bg-accent text-secondary hover:bg-accent-light rounded-md transition-colors font-medium"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-5 h-5"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 5v14M5 12h14"/>
|
||||||
|
</svg>
|
||||||
|
Custom Feed
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<RSSLink lang={lang!} type={type!} />
|
<RSSLink lang={lang!} type={type!} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
22
src/pages/custom.astro
Normal file
22
src/pages/custom.astro
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
import Layout from '../layouts/Layout.astro';
|
||||||
|
import SlugInput from '../components/SlugInput.astro';
|
||||||
|
|
||||||
|
const title = "ComicK - Custom RSS Feed";
|
||||||
|
const description = "Create a custom RSS feed for your favorite comics";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={title} description={description}>
|
||||||
|
<main class="mx-auto px-4 sm:px-6 lg:px-8 max-w-[800px] py-8">
|
||||||
|
<h1 class="text-4xl font-bold text-center mb-8">Custom RSS Feed</h1>
|
||||||
|
|
||||||
|
<div class="bg-secondary/50 p-6 rounded-lg shadow-lg">
|
||||||
|
<SlugInput />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 text-center text-primary/60">
|
||||||
|
<p>Add the comic slugs from the URLs of your favorite comics on ComicK.</p>
|
||||||
|
<p class="mt-2">Example: For "https://comick.io/comic/01-one-piece", the slug is "01-one-piece"</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</Layout>
|
||||||
36
src/pages/rss/custom/[slugs].xml.ts
Normal file
36
src/pages/rss/custom/[slugs].xml.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import rss from "@astrojs/rss";
|
||||||
|
import { getChaptersForSlugs, isValidLanguage } from "../../../lib/api";
|
||||||
|
import type { APIRoute } from "astro";
|
||||||
|
|
||||||
|
export const GET: APIRoute = async ({ params, url }) => {
|
||||||
|
const slugs = params.slugs?.split(',') || [];
|
||||||
|
const lang = url.searchParams.get('lang') || 'en';
|
||||||
|
|
||||||
|
if (slugs.length === 0) {
|
||||||
|
return new Response("No slugs provided", { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidLanguage(lang)) {
|
||||||
|
return new Response("Invalid language", { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Processing RSS request for slugs:', slugs, 'in language:', lang);
|
||||||
|
const chapters = await getChaptersForSlugs(slugs, lang);
|
||||||
|
|
||||||
|
if (chapters.length === 0) {
|
||||||
|
console.log('No chapters found for any of the provided slugs');
|
||||||
|
return new Response("No chapters found for the provided comics", { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return rss({
|
||||||
|
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) => ({
|
||||||
|
title: `${chapter.md_comics.title} - Chapter ${chapter.chap}`,
|
||||||
|
link: `https://comick.io/comic/${chapter.md_comics.slug}`,
|
||||||
|
pubDate: new Date(chapter.updated_at),
|
||||||
|
description: `New chapter available: Chapter ${chapter.chap}`,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue