add specific feeds for specifc manga

This commit is contained in:
ld3zian 2025-01-25 12:37:55 -07:00
parent 0277da93e5
commit 082db594dd
6 changed files with 397 additions and 3 deletions

View 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>

View file

@ -99,4 +99,157 @@ export function getLanguageName(code: string): string {
export const availableLanguages = Array.from(languages);
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;
}

View file

@ -47,7 +47,27 @@ const description = `Latest ${type!.toUpperCase()} comics in ${getLanguageName(l
</h1>
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8">
<LanguageSelector currentLang={lang!} currentType={type!} />
<div class="flex items-center gap-4">
<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!} />
</div>

22
src/pages/custom.astro Normal file
View 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>

View 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}`,
})),
});
};