Skip to content

Commit c239cb2

Browse files
authored
Mangabox: Chapter Retrieval and ID Changes (#156)
* fix(selectors): update selectors to reflect site changes. Author and Chapter List selectors are now different on all MangaBox sites. * refactor: switch to manga & chapter id matching and fix chapters not found. MangaBox sites now pull Chapters via API request and are displayed via js script. Due to this change, the move from full urls as IDs to now Manga and Chapter IDs was done in-order to simplify interaction with the API. * chore: update base major version and reset minor & patch versions on sources.
1 parent a03fc8c commit c239cb2

6 files changed

Lines changed: 78 additions & 55 deletions

File tree

src/MangaBat/MangaBat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
const SITE_DOMAIN = 'https://www.mangabats.com'
1313

1414
export const MangaBatInfo: SourceInfo = {
15-
version: getExportVersion('4.0.2'),
15+
version: getExportVersion('4.0.0'),
1616
name: 'MangaBat',
1717
icon: 'icon.png',
1818
author: 'Batmeow',

src/MangaBox.ts

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
resetSettings
3131
} from './MangaBoxSettings'
3232

33-
const BASE_VERSION = '1.0.1'
33+
const BASE_VERSION = '2.0.0'
3434
export const getExportVersion = (EXTENSION_VERSION: string): string => {
3535
return BASE_VERSION.split('.').map((x, index) => Number(x) + Number(EXTENSION_VERSION.split('.')[index])).join('.')
3636
}
@@ -40,6 +40,14 @@ export interface HomeSectionsParams {
4040
values: [latest: string, newest: string, popular: string]
4141
}
4242

43+
export interface APIChapter {
44+
chapter_name: string
45+
chapter_slug: string
46+
chapter_num: number
47+
updated_at: string
48+
view: number
49+
}
50+
4351
export abstract class MangaBox implements SearchResultsProviding, MangaProviding, ChapterProviding, HomePageSectionsProviding {
4452
// Website base URL. Eg. https://manganato.com
4553
abstract baseURL: string
@@ -93,7 +101,7 @@ export abstract class MangaBox implements SearchResultsProviding, MangaProviding
93101

94102
// Selector for manga author.
95103
mangaAuthorSelector = 'div.story-info-right td:contains(Author) + td a,'
96-
+ 'ul.manga-info-text li:contains(Author) a'
104+
+ 'ul.manga-info-text li:contains(Author)'
97105

98106
// Selector for manga description.
99107
mangaDescSelector = 'div.leftCol div#contentBox, div.chapter + div#contentBox, div#panel-story-info-description, div.manga-info-top + div#contentBox'
@@ -103,8 +111,8 @@ export abstract class MangaBox implements SearchResultsProviding, MangaProviding
103111
+ 'ul.manga-info-text li:contains(Genres) a'
104112

105113
// Selector for manga chapter list.
106-
chapterListSelector = 'div.panel-story-chapter-list ul.row-content-chapter li,'
107-
+ 'div.manga-info-chapter div.chapter-list div.row'
114+
chapterListSelector = 'div#chapter div.manga-info-chapter div#chapter-list-container div.chapter-list div.row,'
115+
+ 'div.panel-story-chapter-list ul.row-content-chapter li'
108116

109117
// Selector for manga chapter time updated.
110118
chapterTimeSelector = 'span.chapter-time, span:last-of-type'
@@ -150,7 +158,7 @@ export abstract class MangaBox implements SearchResultsProviding, MangaProviding
150158
}))
151159
}
152160

153-
getMangaShareUrl(mangaId: string): string { return `${mangaId}` }
161+
getMangaShareUrl(mangaId: string): string { return `${this.baseURL}/manga/${mangaId}/` }
154162

155163
async getHomePageSections(sectionCallback: (section: HomeSection) => void): Promise<void> {
156164
const sections = [
@@ -223,7 +231,10 @@ export abstract class MangaBox implements SearchResultsProviding, MangaProviding
223231

224232
async getMangaDetails(mangaId: string): Promise<SourceManga> {
225233
const request = App.createRequest({
226-
url: `${mangaId}`,
234+
url: new URLBuilder(this.baseURL)
235+
.addPathComponent('manga')
236+
.addPathComponent(mangaId)
237+
.buildUrl(),
227238
method: 'GET'
228239
})
229240

@@ -234,17 +245,44 @@ export abstract class MangaBox implements SearchResultsProviding, MangaProviding
234245
return this.parser.parseMangaDetails($, mangaId, this)
235246
}
236247

237-
async getChapters(mangaId: string): Promise<Chapter[]> {
248+
async getChaptersAPI(mangaId: string, limit = 50, offset: number): Promise<any> {
238249
const request = App.createRequest({
239-
url: `${mangaId}`,
250+
url: new URLBuilder(this.baseURL)
251+
.addPathComponent('api')
252+
.addPathComponent('manga')
253+
.addPathComponent(mangaId)
254+
.addPathComponent('chapters')
255+
.addQueryParameter('limit', limit.toString())
256+
.addQueryParameter('offset', offset.toString())
257+
.buildUrl(),
240258
method: 'GET'
241259
})
242260

243261
const response = await this.requestManager.schedule(request, 1)
244262
this.checkResponseError(response)
245263

246-
const $ = this.cheerio.load(response.data as string)
247-
return this.parser.parseChapters($, mangaId, this)
264+
if (!response.data) throw new Error('No data received from Chapter API')
265+
return JSON.parse(response.data)
266+
}
267+
268+
async getChapters(mangaId: string): Promise<Chapter[]> {
269+
const apiChapters: APIChapter[] = []
270+
let offset = 0
271+
let hasMore = true
272+
273+
while (hasMore) {
274+
const chapters_api_data = await this.getChaptersAPI(mangaId, 50, offset)
275+
if (!chapters_api_data.success) throw new Error('API did not return success for chapters request')
276+
apiChapters.push(...chapters_api_data.data.chapters)
277+
278+
if (!chapters_api_data.data.pagination.has_more) {
279+
hasMore = false
280+
break
281+
}
282+
offset += 50
283+
}
284+
285+
return this.parser.parseChapters(apiChapters, mangaId, this)
248286
}
249287

250288
async getChapterDetails(mangaId: string, chapterId: string): Promise<ChapterDetails> {
@@ -253,7 +291,11 @@ export abstract class MangaBox implements SearchResultsProviding, MangaProviding
253291
const imageServer = await getImageServer(this.stateManager).then(value => value[0])
254292

255293
const request = App.createRequest({
256-
url: `${chapterId}`,
294+
url: new URLBuilder(this.baseURL)
295+
.addPathComponent('manga')
296+
.addPathComponent(mangaId)
297+
.addPathComponent(chapterId)
298+
.buildUrl(),
257299
method: 'GET',
258300
cookies: [
259301
App.createCookie({

src/MangaBoxParser.ts

Lines changed: 21 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import {
99

1010
import { decodeHTML } from 'entities'
1111

12-
import { MangaBox } from './MangaBox'
12+
import {
13+
APIChapter,
14+
MangaBox
15+
} from './MangaBox'
1316

1417
import { getImageServer } from './MangaBoxSettings'
1518

@@ -19,7 +22,7 @@ export class MangaBoxParser {
1922
const collecedIds: string[] = []
2023

2124
for (const manga of $(source.mangaListSelector).toArray()) {
22-
const mangaId = $('a', manga).first().attr('href')
25+
const mangaId = this.idCleaner($('a', manga).attr('href') ?? '')
2326
const image = $('img', manga).first().attr('src')?.trim() ?? ''
2427
const title = decodeHTML($('a', manga).first().attr('title')?.trim() ?? '')
2528
const subtitle = $(source.mangaSubtitleSelector, manga).first().text().trim() ?? ''
@@ -67,11 +70,7 @@ export class MangaBoxParser {
6770
break
6871
}
6972

70-
const author = $(source.mangaAuthorSelector, mangaRootSelector)
71-
.toArray()
72-
.map(x => $(x).text().trim())
73-
.join(', ') ?? ''
74-
73+
const author = $(source.mangaAuthorSelector, mangaRootSelector).first().text().replace('Author(s) :', '').trim()
7574
const desc = decodeHTML($(source.mangaDescSelector).first().children().remove().end().text().trim())
7675

7776
const tags: Tag[] = []
@@ -103,23 +102,17 @@ export class MangaBoxParser {
103102
})
104103
}
105104

106-
parseChapters = ($: CheerioStatic, mangaId: string, source: MangaBox): Chapter[] => {
105+
parseChapters = (apiChapters: APIChapter[], mangaId: string, source: MangaBox): Chapter[] => {
107106
const chapters: Chapter[] = []
108107
let sortingIndex = 0
109108

110-
for (const chapter of $(source.chapterListSelector).toArray()) {
111-
const id = $('a', chapter).attr('href') ?? ''
109+
for (const chapter of apiChapters) {
110+
const id = chapter.chapter_slug ?? ''
112111
if (!id) continue
113112

114-
const name = decodeHTML($('a', chapter).text().trim())
115-
const timeText = $(source.chapterTimeSelector, chapter).text().trim()
116-
const time = this.parseDate(
117-
(timeText.includes('a') ? timeText : $(source.chapterTimeSelector, chapter).attr('title')) ?? ''
118-
)
119-
120-
let chapNum = 0
121-
const chapRegex = id.match(/(?:chap.*)[-_](\d+\.?\d?)/)
122-
if (chapRegex && chapRegex[1]) chapNum = Number(chapRegex[1].replace(/\\/g, '.'))
113+
const name = decodeHTML(chapter.chapter_name?.trim() ?? '')
114+
const time = new Date(chapter.updated_at?.trim() ?? '')
115+
const chapNum = chapter.chapter_num ?? 0
123116

124117
chapters.push({
125118
id: id,
@@ -162,7 +155,6 @@ export class MangaBoxParser {
162155
for (const url of cdns) image = image.replace(url, cdns[imageServer])
163156
}
164157
}
165-
166158
pages.push(image)
167159
}
168160

@@ -200,26 +192,6 @@ export class MangaBoxParser {
200192
return TagSection
201193
}
202194

203-
parseDate = (date: string): Date => {
204-
let time: Date
205-
let number = Number((/\d*/.exec(date) ?? [])[0])
206-
number = (number == 0 && date.includes('a')) ? 1 : number
207-
date = date.toUpperCase()
208-
if (date.includes('MINUTE') || date.includes('MINUTES') || date.includes('MINS')) {
209-
time = new Date(Date.now() - (number * 60000))
210-
} else if (date.includes('HOUR') || date.includes('HOURS')) {
211-
time = new Date(Date.now() - (number * 3600000))
212-
} else if (date.includes('DAY') || date.includes('DAYS')) {
213-
time = new Date(Date.now() - (number * 86400000))
214-
} else if (date.includes('YEAR') || date.includes('YEARS')) {
215-
time = new Date(Date.now() - (number * 31556952000))
216-
} else {
217-
time = new Date(`${date} UTC`)
218-
}
219-
220-
return time
221-
}
222-
223195
isLastPage = ($: CheerioStatic): boolean => {
224196
const currentPage = $('.page-select, .page_select').text()
225197
let totalPages = $('.page-last, .page_last').text()
@@ -231,4 +203,13 @@ export class MangaBoxParser {
231203

232204
return true
233205
}
206+
207+
idCleaner(str: string): string {
208+
let cleanId: string | null = str
209+
cleanId = cleanId.replace(/\/$/, '')
210+
cleanId = cleanId.split('/').pop() ?? null
211+
212+
if (!cleanId) throw new Error(`Unable to parse id for ${str}`) // Log to logger
213+
return cleanId
214+
}
234215
}

src/MangakakalotGG/MangakakalotGG.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
const SITE_DOMAIN = 'https://www.mangakakalot.gg'
1313

1414
export const MangakakalotGGInfo: SourceInfo = {
15-
version: getExportVersion('0.1.4'),
15+
version: getExportVersion('0.0.0'),
1616
name: 'MangakakalotGG',
1717
icon: 'icon.png',
1818
author: 'Batmeow',

src/Manganato/Manganato.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
const SITE_DOMAIN = 'https://www.manganato.gg'
1313

1414
export const ManganatoInfo: SourceInfo = {
15-
version: getExportVersion('4.0.2'),
15+
version: getExportVersion('4.0.0'),
1616
name: 'Manganato',
1717
icon: 'icon.png',
1818
author: 'Batmeow',

src/Natomanga/Natomanga.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
const SITE_DOMAIN = 'https://www.natomanga.com'
1313

1414
export const NatomangaInfo: SourceInfo = {
15-
version: getExportVersion('0.1.4'),
15+
version: getExportVersion('0.0.0'),
1616
name: 'Natomanga',
1717
icon: 'icon.png',
1818
author: 'Batmeow',

0 commit comments

Comments
 (0)