Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

# testing
/coverage
/scripts/
!/scripts/update-readme-health.ts

# next.js
/.next/
Expand All @@ -33,4 +35,4 @@ yarn-error.log*

# typescript
*.tsbuildinfo
next-env.d.ts
next-env.d.ts
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ npm run dev
Runs on http://localhost:3000

**Production:**

```bash
npm run build
npm start
Expand Down Expand Up @@ -135,6 +136,17 @@ Get chapter list for a manga.
}
```

### POST /api/pages

Get chapter page images

```json
{
"url": "https://kaliscan.me/manga/62786-lying-puppies-get-eaten/chapter-1",
"source": "mangapark" // optional, auto-detected
}
```

### GET /api/health

Check source status. Cached for 5 minutes. Returns `cached` and `cacheAge` fields.
Expand Down Expand Up @@ -183,6 +195,7 @@ Fetch frontpage section data from a source.
```

**Available sections for Comix:**

- `trending` - Most Recent Popular (supports time filter)
- `most_followed` - Most Followed New Comics (supports time filter)
- `latest_hot` - Latest Updates (Hot)
Expand All @@ -194,7 +207,7 @@ Fetch frontpage section data from a source.

Proxy requests to AsuraScans and WeebCentral (CORS workaround).

```
```txt
GET /api/proxy/html?url=https://asuracomic.net/...
```

Expand Down
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions src/app/api/pages/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from "next/server";
import { getScraper, getScraperByName } from "@/lib/scrapers";

export const runtime = "edge";

export async function POST(request: NextRequest) {
try {
const { url, source } = await request.json();

if (!url) {
return NextResponse.json({ error: "URL is required" }, { status: 400 });
}

let scraper;

if (source) {
scraper = getScraperByName(source);
}

if (!scraper) {
scraper = getScraper(url);
}

if (!scraper) {
return NextResponse.json(
{
error:
"No scraper found for this URL. Please provide a valid chapter URL or source name.",
},
{ status: 400 },
);
}

if (!scraper.supportsChapterImages()) {
return NextResponse.json(
{
error: `${scraper.getName()} does not support fetching chapter images`,
},
{ status: 400 },
);
}

const images = await scraper.getChapterImages(url);

return NextResponse.json({
images,
source: scraper.getName(),
totalPages: images.length,
});
} catch (error: unknown) {
console.error("Chapter images error:", error);
return NextResponse.json(
{
error:
error instanceof Error
? error.message
: "Failed to fetch chapter images",
},
{ status: 500 },
);
}
}
19 changes: 19 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,25 @@ const endpoints: EndpointProps[] = [
],
"source": "MangaPark",
"totalChapters": 179
}`,
},
{
method: "POST",
path: "/api/pages",
description: "Get chapter page images",
request: `{
"url": "https://kaliscan.com/manga/12345-some-manga/chapter-1",
"source": "kaliscan" // optional
}`,
response: `{
"images": [
{
"url": "https://...",
"page": 1
}
],
"source": "KaliScan",
"totalPages": 20
}`,
},
{
Expand Down
21 changes: 20 additions & 1 deletion src/lib/scrapers/arvencomics.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as cheerio from "cheerio";
import { BaseScraper } from "./base";
import { ScrapedChapter, SearchResult, SourceType } from "@/types";
import { ChapterImage, ScrapedChapter, SearchResult, SourceType } from "@/types";

export class ArvenComicsScraper extends BaseScraper {
private readonly BASE_URL = "https://arvencomics.com";
Expand Down Expand Up @@ -104,6 +104,25 @@ export class ArvenComicsScraper extends BaseScraper {
return chapters.sort((a, b) => a.number - b.number);
}

override supportsChapterImages(): boolean {
return true;
}

async getChapterImages(chapterUrl: string): Promise<ChapterImage[]> {
const html = await this.fetchWithRetry(chapterUrl);
const $ = cheerio.load(html);
const images: ChapterImage[] = [];

$(".reading-content .page-break img, .reading-content img.wp-manga-chapter-img").each((_, el) => {
const url = $(el).attr("data-src")?.trim() || $(el).attr("src")?.trim();
if (url && !url.includes("loading") && !url.includes("placeholder")) {
images.push({ url, page: images.length + 1 });
}
});

return images;
}

protected extractChapterNumber(
chapterUrl: string,
chapterText?: string,
Expand Down
21 changes: 20 additions & 1 deletion src/lib/scrapers/arya-scans.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as cheerio from "cheerio";
import { BaseScraper } from "./base";
import { ScrapedChapter, SearchResult, SourceType } from "@/types";
import { ChapterImage, ScrapedChapter, SearchResult, SourceType } from "@/types";

export class AryaScansScraper extends BaseScraper {
private readonly BASE_URL = "https://brainrotcomics.com";
Expand Down Expand Up @@ -108,6 +108,25 @@ export class AryaScansScraper extends BaseScraper {
return chapters.sort((a, b) => a.number - b.number);
}

override supportsChapterImages(): boolean {
return true;
}

async getChapterImages(chapterUrl: string): Promise<ChapterImage[]> {
const html = await this.fetchWithRetry(chapterUrl);
const $ = cheerio.load(html);
const images: ChapterImage[] = [];

$(".reading-content .page-break img, .reading-content img.wp-manga-chapter-img").each((_, el) => {
const url = $(el).attr("data-src")?.trim() || $(el).attr("src")?.trim();
if (url && !url.includes("loading") && !url.includes("placeholder")) {
images.push({ url, page: images.length + 1 });
}
});

return images;
}

protected extractChapterNumber(chapterUrl: string): number {
const patterns = [
/chapter[/-](\d+)(?:[.-](\d+))?/i,
Expand Down
21 changes: 20 additions & 1 deletion src/lib/scrapers/asmotoon.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as cheerio from "cheerio";
import { BaseScraper } from "./base";
import { ScrapedChapter, SearchResult, SourceType } from "@/types";
import { ChapterImage, ScrapedChapter, SearchResult, SourceType } from "@/types";

export class AsmotoonScraper extends BaseScraper {
private readonly BASE_URL = "https://asmotoon.com";
Expand Down Expand Up @@ -107,6 +107,25 @@ export class AsmotoonScraper extends BaseScraper {
return -1;
}

override supportsChapterImages(): boolean {
return true;
}

async getChapterImages(chapterUrl: string): Promise<ChapterImage[]> {
const html = await this.fetchWithRetry(chapterUrl);
const $ = cheerio.load(html);
const images: ChapterImage[] = [];

$("#pages img[uid]").each((_, el) => {
const uid = $(el).attr("uid")?.trim();
if (uid) {
images.push({ url: `https://cdn.meowing.org/uploads/${uid}`, page: images.length + 1 });
}
});

return images;
}

async search(query: string): Promise<SearchResult[]> {
try {
const searchUrl = `${this.BASE_URL}/series?q=${encodeURIComponent(query)}`;
Expand Down
Loading