-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
350 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
import { | ||
SourceManga, | ||
Chapter, | ||
ChapterDetails, | ||
HomeSection, | ||
SearchRequest, | ||
PagedResults, | ||
SourceInfo, | ||
ContentRating, | ||
Request, | ||
Response, | ||
SourceIntents, | ||
ChapterProviding, | ||
MangaProviding, | ||
SearchResultsProviding, | ||
HomePageSectionsProviding | ||
} from '@paperback/types' | ||
|
||
import * as cheerio from 'cheerio' | ||
|
||
import { | ||
parseChapterDetails, | ||
isLastPage, | ||
parseChapters, | ||
parseHomeSections, | ||
parseMangaDetails, | ||
parseViewMore, | ||
parseSearchResults | ||
} from './ReadAllComicsParser' | ||
|
||
const RC_DOMAIN = 'https://readallcomics.com' | ||
|
||
export const ReadAllComicsInfo: SourceInfo = { | ||
version: '1.0.0', | ||
name: 'ReadAllComics', | ||
icon: 'icon.png', | ||
author: 'Netsky', | ||
authorWebsite: 'https://github.com/TheNetsky', | ||
description: 'Extension that pulls manga from readallcomics.com', | ||
contentRating: ContentRating.EVERYONE, | ||
websiteBaseURL: RC_DOMAIN, | ||
intents: SourceIntents.MANGA_CHAPTERS | SourceIntents.HOMEPAGE_SECTIONS | SourceIntents.CLOUDFLARE_BYPASS_REQUIRED | ||
} | ||
|
||
export class ReadAllComics implements SearchResultsProviding, MangaProviding, ChapterProviding, HomePageSectionsProviding { | ||
|
||
requestManager = App.createRequestManager({ | ||
requestsPerSecond: 4, | ||
requestTimeout: 15000, | ||
interceptor: { | ||
interceptRequest: async (request: Request): Promise<Request> => { | ||
request.headers = { | ||
...(request.headers ?? {}), | ||
...{ | ||
'referer': `${RC_DOMAIN}/`, | ||
'user-agent': await this.requestManager.getDefaultUserAgent() | ||
} | ||
} | ||
request.url = request.url.replace(/^http:/, 'https:') | ||
return request | ||
}, | ||
interceptResponse: async (response: Response): Promise<Response> => { | ||
return response | ||
} | ||
} | ||
}); | ||
|
||
getMangaShareUrl(mangaId: string): string { return `${RC_DOMAIN}/category/${mangaId}` } | ||
|
||
async getMangaDetails(mangaId: string): Promise<SourceManga> { | ||
const request = App.createRequest({ | ||
url: `${RC_DOMAIN}/category/${mangaId}`, | ||
method: 'GET' | ||
}) | ||
|
||
const response = await this.requestManager.schedule(request, 1) | ||
this.CloudFlareError(response.status) | ||
const $ = cheerio.load(response.data as string) | ||
return parseMangaDetails($, mangaId) | ||
} | ||
|
||
async getChapters(mangaId: string): Promise<Chapter[]> { | ||
const request = App.createRequest({ | ||
url: `${RC_DOMAIN}/category/${mangaId}`, | ||
method: 'GET' | ||
}) | ||
|
||
const response = await this.requestManager.schedule(request, 1) | ||
this.CloudFlareError(response.status) | ||
const $ = cheerio.load(response.data as string) | ||
return parseChapters($, mangaId) | ||
} | ||
|
||
async getChapterDetails(mangaId: string, chapterId: string): Promise<ChapterDetails> { | ||
const request = App.createRequest({ | ||
url: `${RC_DOMAIN}/${chapterId}`, | ||
method: 'GET' | ||
}) | ||
|
||
const response = await this.requestManager.schedule(request, 1) | ||
this.CloudFlareError(response.status) | ||
const $ = cheerio.load(response.data as string) | ||
return parseChapterDetails($, mangaId, chapterId) | ||
} | ||
|
||
async getHomePageSections(sectionCallback: (section: HomeSection) => void): Promise<void> { | ||
const request = App.createRequest({ | ||
url: RC_DOMAIN, | ||
method: 'GET' | ||
}) | ||
|
||
const response = await this.requestManager.schedule(request, 1) | ||
this.CloudFlareError(response.status) | ||
const $ = cheerio.load(response.data as string) | ||
parseHomeSections($, sectionCallback) | ||
} | ||
|
||
async getViewMoreItems(homepageSectionId: string, metadata: any): Promise<PagedResults> { | ||
if (metadata?.completed) return metadata | ||
|
||
const page: number = metadata?.page ?? 1 | ||
let param = '' | ||
|
||
switch (homepageSectionId) { | ||
case 'new_updated': | ||
param = `/page/${page}` | ||
break | ||
default: | ||
throw new Error('Requested to getViewMoreItems for a section ID which doesn\'t exist') | ||
} | ||
|
||
const request = App.createRequest({ | ||
url: `${RC_DOMAIN}/${param}`, | ||
method: 'GET' | ||
}) | ||
|
||
const response = await this.requestManager.schedule(request, 1) | ||
this.CloudFlareError(response.status) | ||
const $ = cheerio.load(response.data as string) | ||
const comics = parseViewMore($) | ||
|
||
metadata = !isLastPage($) ? { page: page + 1 } : undefined | ||
return App.createPagedResults({ | ||
results: comics, | ||
metadata | ||
}) | ||
} | ||
|
||
async getSearchResults(query: SearchRequest, metadata: any): Promise<PagedResults> { | ||
const request = App.createRequest({ | ||
url: `${RC_DOMAIN}/?story=${encodeURI(query.title ?? '')}&s=&type=comic`, | ||
method: 'GET' | ||
}) | ||
|
||
const response = await this.requestManager.schedule(request, 1) | ||
const $ = cheerio.load(response.data as string) | ||
const manga = parseSearchResults($) | ||
|
||
return App.createPagedResults({ | ||
results: manga, | ||
metadata | ||
}) | ||
} | ||
|
||
CloudFlareError(status: number): void { | ||
if (status == 503 || status == 403) { | ||
throw new Error(`CLOUDFLARE BYPASS ERROR:\nPlease go to the homepage of <${ReadAllComics.name}> and press the cloud icon.`) | ||
} | ||
} | ||
|
||
async getCloudflareBypassRequestAsync(): Promise<Request> { | ||
return App.createRequest({ | ||
url: RC_DOMAIN, | ||
method: 'GET', | ||
headers: { | ||
'referer': `${RC_DOMAIN}/`, | ||
'user-agent': await this.requestManager.getDefaultUserAgent() | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
import { | ||
Chapter, | ||
ChapterDetails, | ||
HomeSection, | ||
SourceManga, | ||
PartialSourceManga, | ||
HomeSectionType | ||
} from '@paperback/types' | ||
|
||
import { decode as decodeHTMLEntity } from 'html-entities' | ||
|
||
import { CheerioAPI } from 'cheerio' | ||
|
||
|
||
export const parseMangaDetails = ($: CheerioAPI, mangaId: string): SourceManga => { | ||
const title: string = decodeHTMLEntity($('h1').text()?.trim() ?? '') | ||
|
||
const image = $('img', '.description-archive').first().attr('src') ?? '' | ||
const description = $('div.b > strong', '.description-archive').text().trim() | ||
const publisher = $('strong', 'p:contains(Publisher:)').last().text().trim() ?? '' | ||
|
||
return App.createSourceManga({ | ||
id: mangaId, | ||
mangaInfo: App.createMangaInfo({ | ||
titles: [title], | ||
image: image, | ||
status: 'Ongoing', | ||
artist: publisher, | ||
author: publisher, | ||
desc: decodeHTMLEntity(description) | ||
}) | ||
}) | ||
} | ||
|
||
export const parseChapters = ($: CheerioAPI, mangaId: string): Chapter[] => { | ||
const chapters: Chapter[] = [] | ||
let sortingIndex = 0 | ||
|
||
const chapterListElement = $('li', 'ul.list-story').toArray() | ||
let i = chapterListElement.length | ||
|
||
for (const chapter of chapterListElement) { | ||
const id = $('a', chapter).attr('href')?.replace(/\/$/, '').split('/').pop() ?? '' | ||
|
||
if (!id) continue | ||
|
||
const title = $('a', chapter).text().trim() ?? '' | ||
|
||
chapters.push(App.createChapter({ | ||
id: id, | ||
name: title, | ||
langCode: '🇬🇧', | ||
chapNum: i, | ||
sortingIndex, | ||
time: new Date() | ||
})) | ||
sortingIndex-- | ||
i-- | ||
} | ||
|
||
if (chapters.length == 0) { | ||
throw new Error(`Couldn't find any chapters for mangaId: ${mangaId}!`) | ||
} | ||
|
||
return chapters | ||
} | ||
|
||
export const parseChapterDetails = ($: CheerioAPI, mangaId: string, chapterId: string): ChapterDetails => { | ||
const pages: string[] = [] | ||
|
||
for (const img of $('img', 'center > div:nth-child(2)').toArray()) { | ||
const image = $(img).attr('src') ?? '' | ||
if (!image) continue | ||
pages.push(image.trim()) | ||
} | ||
|
||
const chapterDetails = App.createChapterDetails({ | ||
id: chapterId, | ||
mangaId: mangaId, | ||
pages: pages | ||
}) | ||
return chapterDetails | ||
} | ||
|
||
export const parseHomeSections = ($: CheerioAPI, sectionCallback: (section: HomeSection) => void): void => { | ||
// New Updated Comics | ||
const newCompletedSection = App.createHomeSection({ | ||
id: 'new_updated', | ||
title: 'Newly Updated Comics', | ||
containsMoreItems: true, | ||
type: HomeSectionType.singleRowLarge | ||
}) | ||
|
||
const newCompletedSection_Array: PartialSourceManga[] = [] | ||
for (const item of $('div', 'div#post-area').toArray()) { | ||
const classAttribute = $(item).attr('class') ?? '' | ||
|
||
const idRegex = classAttribute?.match(/category-([^ ]+)/) | ||
let id = '' | ||
if (idRegex && idRegex[1]) id = idRegex[1] | ||
|
||
const title: string = $('h2', item).text().trim() ?? '' | ||
const subtitle: string = $('span', item).first().text().trim() | ||
const image = $('img', item).attr('src') ?? '' | ||
|
||
if (!id || !title) continue | ||
newCompletedSection_Array.push(App.createPartialSourceManga({ | ||
image: encodeURI(image), | ||
title: decodeHTMLEntity(title), | ||
mangaId: id, | ||
subtitle: decodeHTMLEntity(subtitle) | ||
})) | ||
} | ||
newCompletedSection.items = newCompletedSection_Array | ||
sectionCallback(newCompletedSection) | ||
} | ||
|
||
export const parseViewMore = ($: CheerioAPI): PartialSourceManga[] => { | ||
const comics: PartialSourceManga[] = [] | ||
const collectedIds: string[] = [] | ||
|
||
for (const item of $('div', 'div#post-area').toArray()) { | ||
const classAttribute = $(item).attr('class') ?? '' | ||
|
||
const idRegex = classAttribute?.match(/category-([^ ]+)/) | ||
let id = '' | ||
if (idRegex && idRegex[1]) id = idRegex[1] | ||
|
||
const title: string = $('h2', item).text().trim() ?? '' | ||
const subtitle: string = $('span', item).first().text().trim() | ||
const image = $('img', item).attr('src') ?? '' | ||
|
||
if (!id || !title || collectedIds.includes(id)) continue | ||
comics.push(App.createPartialSourceManga({ | ||
image: encodeURI(image), | ||
title: decodeHTMLEntity(title), | ||
mangaId: id, | ||
subtitle: decodeHTMLEntity(subtitle) | ||
})) | ||
} | ||
return comics | ||
} | ||
|
||
export const parseSearchResults = ($: CheerioAPI): PartialSourceManga[] => { | ||
const comics: PartialSourceManga[] = [] | ||
const collectedIds: string[] = [] | ||
|
||
for (const item of $('li', 'ul.list-story').toArray()) { | ||
const id = $('a', item).attr('href')?.replace(/\/$/, '').split('/').pop() ?? '' | ||
const title: string = $(item).text().trim() ?? '' | ||
|
||
if (!id || !title || collectedIds.includes(id)) continue | ||
comics.push(App.createPartialSourceManga({ | ||
image: '', // There is no image in search results | ||
title: decodeHTMLEntity(title), | ||
mangaId: id | ||
})) | ||
} | ||
return comics | ||
} | ||
|
||
export const isLastPage = ($: CheerioAPI): boolean => { | ||
let isLast = false | ||
|
||
const lastPage = $('a:contains(Next)') ?? '' | ||
if (!lastPage) isLast = true | ||
|
||
return isLast | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.