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

Commit e9e27c2

Browse files
committed
Add VoidScans
1 parent 5ac71e8 commit e9e27c2

File tree

4 files changed

+258
-1
lines changed

4 files changed

+258
-1
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ Sources for small scanlation sites.
33

44
# Websites
55
* https://glitchycomics.com/
6-
* https://rainofsnow.com/
6+
* https://rainofsnow.com/
7+
* https://voidscans.net/

src/VoidScans/VoidScans.ts

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import {
2+
Chapter,
3+
ChapterDetails,
4+
HomeSection,
5+
Manga, MangaTile,
6+
PagedResults,
7+
Request,
8+
SearchRequest,
9+
Source,
10+
SourceInfo,
11+
} from "paperback-extensions-common"
12+
import {VoidScansParser} from "./VoidScansParser";
13+
14+
const BASE = "https://voidscans.net"
15+
16+
export const VoidScansInfo: SourceInfo = {
17+
icon: "icon.png",
18+
version: "1.1.0",
19+
name: "VoidScans",
20+
author: "PythonCoderAS",
21+
authorWebsite: "https://github.com/PythonCoderAS",
22+
description: "Extension that pulls manga from VoidScans",
23+
language: "en",
24+
hentaiSource: false,
25+
websiteBaseURL: BASE
26+
}
27+
28+
export class VoidScans extends Source {
29+
30+
private readonly parser: VoidScansParser = new VoidScansParser();
31+
32+
getMangaShareUrl(mangaId: string): string | null {
33+
return `${BASE}/library/${mangaId}`;
34+
}
35+
36+
async getHomePageSections(sectionCallback: (section: HomeSection) => void): Promise<void> {
37+
sectionCallback(createHomeSection({
38+
id: "1",
39+
items: (await this.getWebsiteMangaDirectory(null)).results,
40+
title: "All Manga"
41+
}));
42+
}
43+
44+
async getWebsiteMangaDirectory(metadata: any): Promise<PagedResults> {
45+
const options: Request = createRequestObject({
46+
url: `${BASE}/library`,
47+
method: 'GET'
48+
});
49+
let response = await this.requestManager.schedule(options, 1);
50+
let $ = this.cheerio.load(response.data);
51+
return createPagedResults({
52+
results: this.parser.parseMangaList($, BASE)
53+
});
54+
}
55+
56+
async getChapterPage(mangaId: string, chapterId: string, page: number = 1): Promise<string | null>{
57+
const options: Request = createRequestObject({
58+
url: `${BASE}/read/${mangaId}/${chapterId}/${page}`,
59+
method: 'GET'
60+
});
61+
let response = await this.requestManager.schedule(options, 1);
62+
let $ = this.cheerio.load(response.data);
63+
return this.parser.parsePage($)
64+
}
65+
66+
async getChapterDetails(mangaId: string, chapterId: string): Promise<ChapterDetails> {
67+
const pages: string[] = [];
68+
let page = await this.getChapterPage(mangaId, chapterId);
69+
let num = 2;
70+
while (page){
71+
pages.push(page)
72+
page = await this.getChapterPage(mangaId, chapterId, num);
73+
num++;
74+
}
75+
return createChapterDetails({
76+
id: chapterId,
77+
longStrip: true,
78+
mangaId: mangaId,
79+
pages: pages
80+
})
81+
}
82+
83+
async getChapters(mangaId: string): Promise<Chapter[]> {
84+
const options: Request = createRequestObject({
85+
url: `${BASE}/library/${mangaId}`,
86+
method: 'GET'
87+
});
88+
let response = await this.requestManager.schedule(options, 1);
89+
let $ = this.cheerio.load(response.data);
90+
return this.parser.parseChapterList($, mangaId);
91+
}
92+
93+
async getMangaDetails(mangaId: string): Promise<Manga> {
94+
const options: Request = createRequestObject({
95+
url: `${BASE}/library/${mangaId}`,
96+
method: 'GET'
97+
});
98+
let response = await this.requestManager.schedule(options, 1);
99+
let $ = this.cheerio.load(response.data);
100+
return this.parser.parseManga($, mangaId);
101+
}
102+
103+
async searchRequest(query: SearchRequest, metadata: any): Promise<PagedResults> {
104+
// TODO: Wait for search to be implemented on the website.
105+
const results = (await this.getWebsiteMangaDirectory(null)).results;
106+
const data: MangaTile[] = [];
107+
for (let i = 0; i < results.length; i++) {
108+
const key = results[i];
109+
if (query.title) {
110+
if ((key.primaryText?.text || "").toLowerCase().includes((query.title.toLowerCase()))) {
111+
data.push(key);
112+
}
113+
}
114+
}
115+
return createPagedResults({
116+
results: data
117+
});
118+
}
119+
}

src/VoidScans/VoidScansParser.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {Chapter, LanguageCode, Manga, MangaStatus, MangaTile} from "paperback-extensions-common";
2+
3+
export class VoidScansParser {
4+
parseMangaList($: CheerioStatic, base: string) {
5+
const mangaTiles: MangaTile[] = [];
6+
$("div.col").map((index, element) => {
7+
const link = $("a.btn", element);
8+
const linkId = link.attr("href");
9+
if (linkId){
10+
mangaTiles.push(createMangaTile({
11+
id: linkId.replace(`${base}/library/`, ""),
12+
title: createIconText({
13+
text: ""
14+
}),
15+
image: $("img", element).attr("src") || "",
16+
primaryText: createIconText({
17+
text: $("p.card-text", element).text()
18+
})
19+
}))
20+
}
21+
})
22+
return mangaTiles;
23+
}
24+
25+
parsePage($: CheerioStatic): string | null {
26+
return $("img").attr("src") || null;
27+
}
28+
29+
parseChapterList($: CheerioStatic, mangaId: string) {
30+
const chapters: Chapter[] = [];
31+
$("ul.list-group").first().children().map((index, element) => {
32+
const link = $(element).first();
33+
const chapNum = Number(link.text().replace("Chapter ", ""));
34+
const data: Chapter = {
35+
chapNum: chapNum,
36+
id: String(chapNum),
37+
langCode: LanguageCode.ENGLISH,
38+
mangaId: mangaId,
39+
}
40+
chapters.push(createChapter(data))
41+
})
42+
return chapters
43+
}
44+
45+
parseManga($: CheerioStatic, mangaId: string) {
46+
const mangaObj: Manga = {
47+
desc: $("p").first().text().trim(),
48+
id: mangaId,
49+
image: $("img#manga-img").attr("src") || "",
50+
rating: 0,
51+
status: MangaStatus.ONGOING,
52+
titles: [$("h1").first().text()],
53+
}
54+
return createManga(mangaObj)
55+
}
56+
57+
}

src/tests/VoidScans.test.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import cheerio from "cheerio";
2+
import {VoidScans} from "../VoidScans/VoidScans";
3+
import {APIWrapper, Source} from "paperback-extensions-common";
4+
5+
describe("VoidScans Tests", function () {
6+
let wrapper: APIWrapper = new APIWrapper();
7+
let source: Source = new VoidScans(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 = "2";
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: "Son",
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+
});

0 commit comments

Comments
 (0)