Skip to content
This repository was archived by the owner on Nov 20, 2021. It is now read-only.

Commit 7fd0525

Browse files
committed
Add CatManga(BlackCatScanlations/WhiteCatScanlations)
1 parent 9ed8ad3 commit 7fd0525

File tree

4 files changed

+342
-0
lines changed

4 files changed

+342
-0
lines changed

src/CatManga/CatManga.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import {
2+
Chapter,
3+
ChapterDetails,
4+
HomeSection,
5+
Manga, MangaTile, MangaUpdates,
6+
PagedResults,
7+
Request,
8+
SearchRequest,
9+
Source,
10+
SourceInfo,
11+
} from "paperback-extensions-common"
12+
import {CatMangaParser} from "./CatMangaParser";
13+
14+
const BASE = "https://catmanga.org"
15+
16+
export const CatMangaInfo: SourceInfo = {
17+
icon: "icon.svg",
18+
version: "1.0.0",
19+
name: "CatManga",
20+
author: "PythonCoderAS",
21+
authorWebsite: "https://github.com/PythonCoderAS",
22+
description: "Extension that pulls manga from CatManga",
23+
language: "en",
24+
hentaiSource: false,
25+
websiteBaseURL: BASE
26+
}
27+
28+
export class CatManga extends Source {
29+
30+
private readonly parser: CatMangaParser = new CatMangaParser();
31+
32+
getMangaShareUrl(mangaId: string): string | null {
33+
return `${BASE}/series/${mangaId}`;
34+
}
35+
36+
async getHomePageSections(sectionCallback: (section: HomeSection) => void): Promise<void> {
37+
const $ = await this.getHomePageData()
38+
sectionCallback(createHomeSection({
39+
id: "featured",
40+
title: "Featured",
41+
items: this.parser.parseFeatured($)
42+
}))
43+
sectionCallback(createHomeSection({
44+
id: "latest",
45+
title: "Latest",
46+
items: this.getLatest($)
47+
}))
48+
sectionCallback(createHomeSection({
49+
id: "all",
50+
items: (await this.getWebsiteMangaDirectory(null)).results,
51+
title: "All Manga"
52+
}));
53+
}
54+
55+
async getHomePageData(): Promise<CheerioStatic> {
56+
const options: Request = createRequestObject({
57+
url: `${BASE}`,
58+
method: 'GET'
59+
});
60+
let response = await this.requestManager.schedule(options, 1);
61+
return this.cheerio.load(response.data);
62+
}
63+
64+
getLatest($: CheerioStatic){
65+
return this.parser.parseTileList($, "latestChapterListView", "latestChapterView");
66+
}
67+
68+
async getWebsiteMangaDirectory(metadata: any): Promise<PagedResults> {
69+
return createPagedResults({
70+
results: this.parser.parseTileList(await this.getHomePageData(), "allseries")
71+
});
72+
}
73+
74+
75+
async getChapterDetails(mangaId: string, chapterId: string): Promise<ChapterDetails> {
76+
const options: Request = createRequestObject({
77+
url: `${BASE}/series/${mangaId}/${chapterId}`,
78+
method: 'GET'
79+
});
80+
let response = await this.requestManager.schedule(options, 1);
81+
let $ = this.cheerio.load(response.data);
82+
return createChapterDetails({
83+
id: chapterId,
84+
longStrip: true,
85+
mangaId: mangaId,
86+
pages: this.parser.parsePages($)
87+
});
88+
}
89+
90+
async getChapters(mangaId: string): Promise<Chapter[]> {
91+
const options: Request = createRequestObject({
92+
url: `${BASE}/series/${mangaId}`,
93+
method: 'GET'
94+
});
95+
let response = await this.requestManager.schedule(options, 1);
96+
let $ = this.cheerio.load(response.data);
97+
return this.parser.parseChapterList($, mangaId);
98+
}
99+
100+
async getMangaDetails(mangaId: string): Promise<Manga> {
101+
const options: Request = createRequestObject({
102+
url: `${BASE}/series/${mangaId}`,
103+
method: 'GET'
104+
});
105+
let response = await this.requestManager.schedule(options, 1);
106+
let $ = this.cheerio.load(response.data);
107+
return this.parser.parseManga($, mangaId);
108+
}
109+
110+
async searchRequest(query: SearchRequest, metadata: any): Promise<PagedResults> {
111+
// TODO: Wait for search to be implemented on the website.
112+
const results = (await this.getWebsiteMangaDirectory(null)).results;
113+
const data: MangaTile[] = [];
114+
for (let i = 0; i < results.length; i++) {
115+
const key = results[i];
116+
if (query.title) {
117+
if ((key.title.text || "").toLowerCase().includes((query.title.toLowerCase()))) {
118+
data.push(key);
119+
}
120+
}
121+
}
122+
return createPagedResults({
123+
results: data
124+
});
125+
}
126+
127+
128+
async filterUpdatedManga(mangaUpdatesFoundCallback: (updates: MangaUpdates) => void, time: Date, ids: string[]): Promise<void> {
129+
const tiles: MangaTile[] = this.getLatest(await this.getHomePageData());
130+
const idsFound: string[] = [];
131+
for (let i = 0; i < tiles.length; i++) {
132+
const id = tiles[i].id;
133+
if (ids.includes(id)){
134+
idsFound.push(id)
135+
}
136+
}
137+
mangaUpdatesFoundCallback(createMangaUpdates({
138+
ids: idsFound
139+
}));
140+
}
141+
}

src/CatManga/CatMangaParser.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import {Chapter, LanguageCode, Manga, MangaStatus, MangaTile, Tag} from "paperback-extensions-common";
2+
3+
export class CatMangaParser {
4+
parseTileList($: CheerioStatic, className: string, className2: string | null = null) {
5+
if (className2 === null){
6+
className2 = className;
7+
}
8+
const mangaTiles: MangaTile[] = [];
9+
$(`div[class^=${className}_grid] *[class^=${className2}_element]`).map((index, element) => {
10+
const linkId = element.attribs["href"];
11+
if (linkId) {
12+
const tile: MangaTile = {
13+
id: linkId.replace(`/series/`, "").split("/")[0],
14+
title: createIconText({
15+
text: $("p", element).first().text().trim()
16+
}),
17+
image: $("img", element).attr("src") || ""
18+
}
19+
if ($("p", element).length > 1){
20+
tile.primaryText = createIconText({
21+
text: $("p", element).last().text().trim()
22+
});
23+
}
24+
mangaTiles.push(createMangaTile(tile));
25+
}
26+
})
27+
return mangaTiles;
28+
}
29+
30+
parseFeatured($: CheerioStatic){
31+
const seen: string[] = [];
32+
const mangaTiles: MangaTile[] = [];
33+
$("ul.slider").map((index, element) => {
34+
const link = $("a", element);
35+
const linkId = link.attr("href")
36+
if (linkId){
37+
const id = linkId.replace(`/series/`, "").split("/")[0];
38+
if (!seen.includes(id)){
39+
seen.push(id);
40+
mangaTiles.push(createMangaTile({
41+
id: id,
42+
title: createIconText({
43+
text: $("h1", element).first().text().trim()
44+
}),
45+
image: $("img", element).attr("src") || "",
46+
primaryText: createIconText({
47+
text: $("div p", $("a", element).parent()).first().text().trim()
48+
})
49+
}))
50+
}
51+
}
52+
})
53+
return mangaTiles;
54+
}
55+
56+
parsePages($: CheerioStatic): string[] {
57+
const json = JSON.parse($("script#__NEXT_DATA__").html() || "{}");
58+
return json.props.pageProps.pages;
59+
}
60+
61+
parseChapterList($: CheerioStatic, mangaId: string) {
62+
const chapters: Chapter[] = [];
63+
$('a[class^="chaptertile_element"]').map((index, element) => {
64+
const chapNum = Number($("p", element).first().text().replace("Chapter ", ""));
65+
const data: Chapter = {
66+
chapNum: chapNum,
67+
id: String(chapNum),
68+
langCode: LanguageCode.ENGLISH,
69+
mangaId: mangaId,
70+
name: $("p", element).last().text().trim()
71+
};
72+
chapters.push(createChapter(data));
73+
})
74+
return chapters
75+
}
76+
77+
parseManga($: CheerioStatic, mangaId: string) {
78+
const tags: Tag[] = [];
79+
$('div[class^="series_tags"] p').map((index, element) => {
80+
const text = $(element).text().trim();
81+
tags.push(createTag({
82+
id: text,
83+
label: text
84+
}));
85+
});
86+
let status;
87+
if ($('p[class^="series_seriesStatus"]').first().text().trim().toLowerCase().includes("ongoing")){
88+
status = MangaStatus.ONGOING;
89+
} else {
90+
status = MangaStatus.COMPLETED;
91+
}
92+
const mangaObj: Manga = {
93+
desc: $('div[class^="series_seriesDesc"]').first().text().trim(),
94+
id: mangaId,
95+
image: $("img").attr("src") || "",
96+
rating: 0,
97+
status: status,
98+
titles: [$("h1").first().text()],
99+
tags: [createTagSection({
100+
id: "tags",
101+
label: "Tags",
102+
tags: tags
103+
})]
104+
}
105+
return createManga(mangaObj)
106+
}
107+
108+
}

src/CatManga/includes/icon.svg

Lines changed: 6 additions & 0 deletions
Loading

src/tests/CatManga.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import cheerio from "cheerio";
2+
import {CatManga} from "../CatManga/CatManga";
3+
import {APIWrapper, Source} from "paperback-extensions-common";
4+
5+
describe("CatManga Tests", function () {
6+
let wrapper: APIWrapper = new APIWrapper();
7+
let source: Source = new CatManga(cheerio);
8+
let chai = require("chai"),
9+
expect = chai.expect;
10+
let chaiAsPromised = require("chai-as-promised");
11+
chai.use(chaiAsPromised);
12+
13+
let mangaId = "kanokari";
14+
15+
it("Retrieve Manga Details", async () => {
16+
let details = await wrapper.getMangaDetails(source, mangaId);
17+
expect(
18+
details,
19+
"No results found with test-defined ID [" + mangaId + "]"
20+
).to.exist;
21+
22+
// Validate that the fields are filled
23+
let data = details;
24+
expect(data.id, "Missing ID").to.be.not.empty;
25+
expect(data.image, "Missing Image").to.exist;
26+
expect(data.status, "Missing Status").to.exist;
27+
expect(data.titles, "Missing Titles").to.be.not.empty;
28+
expect(data.rating, "Missing Rating").to.exist;
29+
expect(data.desc, "Missing Description").to.be.not.empty;
30+
});
31+
32+
it("Get Chapters", async () => {
33+
let data = await wrapper.getChapters(source, mangaId);
34+
35+
expect(data, "No chapters present for: [" + mangaId + "]").to.not.be.empty;
36+
37+
let entry = data[0];
38+
expect(entry.id, "No ID present").to.not.be.empty;
39+
expect(entry.chapNum, "No chapter number present").to.exist;
40+
});
41+
42+
it("Get Chapter Details", async () => {
43+
let chapters = await wrapper.getChapters(source, mangaId);
44+
let data = await wrapper.getChapterDetails(source, mangaId, chapters[0].id);
45+
46+
expect(data, "Empty server response").to.not.be.empty;
47+
48+
expect(data.id, "Missing ID").to.be.not.empty;
49+
expect(data.mangaId, "Missing MangaID").to.be.not.empty;
50+
expect(data.pages, "No pages present").to.be.not.empty;
51+
});
52+
53+
it("Testing search", async () => {
54+
let testSearch = createSearchRequest({
55+
title: "komi",
56+
});
57+
58+
let search = await wrapper.searchRequest(source, testSearch);
59+
let result = search.results[0];
60+
61+
expect(result, "No response from server").to.exist;
62+
63+
expect(result.id, "No ID found for search query").to.be.not.empty;
64+
expect(result.title, "No title").to.be.not.empty;
65+
});
66+
67+
it("Testing Home Page", async () => {
68+
let result = await wrapper.getHomePageSections(source);
69+
expect(result, "No response from server").to.exist;
70+
let item = result[0];
71+
expect(item, "Empty response from server").to.exist;
72+
if (item.items) {
73+
let subitem = item.items[0];
74+
75+
expect(subitem.id, "No ID found for homepage item").to.not.be.empty;
76+
expect(subitem.title, "No Title found for homepage item").to.not.be.empty;
77+
expect(subitem.image, "No Image found for homepage item").to.not.be.empty;
78+
}
79+
})
80+
81+
it("Testing Notifications", async () => {
82+
let updates = await wrapper.filterUpdatedManga(source, new Date("2021-1-27"), [mangaId])
83+
expect(updates, "No server response").to.exist
84+
expect(updates, "Empty server response").to.not.be.empty
85+
expect(updates[0], "No updates").to.not.be.empty;
86+
})
87+
});

0 commit comments

Comments
 (0)