Skip to content

Commit

Permalink
Add ReadAllComics
Browse files Browse the repository at this point in the history
  • Loading branch information
TheNetsky committed Sep 11, 2024
1 parent 9e02365 commit 1b33fc6
Show file tree
Hide file tree
Showing 3 changed files with 350 additions and 0 deletions.
181 changes: 181 additions & 0 deletions src/ReadAllComics/ReadAllComics.ts
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()
}
})
}
}
169 changes: 169 additions & 0 deletions src/ReadAllComics/ReadAllComicsParser.ts
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
}
Binary file added src/ReadAllComics/includes/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 1b33fc6

Please sign in to comment.