mangadex/scripts/reader/api.js
2021-03-19 13:06:32 -07:00

418 lines
13 KiB
JavaScript

import Utils from './utils.js'
import natsort from 'natsort'
export default class Resource {
get resourceType() { return 'resource' }
get resourceFormat() { return 'json' }
constructor(data = {}) {
this.initialize(data)
}
initialize(data = {}) {
this._data = data
}
get status() { return this._data.status }
load(opts = {}, force = false) {
const id = opts.id != null ? opts.id : ''
const type = this.resourceType
return new Promise((resolve, reject) => {
opts.type = type
if (!(type in Resource.cache)) {
Resource.cache[type] = {}
}
if (!force && id in Resource.cache[type]) {
return resolve(Resource.cache[type][id])
}
const baseURL = opts.baseURL || window.location
delete opts.baseURL
const url = new URL(this.constructor.API_URL(), baseURL)
for (let key in opts) {
url.searchParams.append(key, opts[key])
}
return resolve(fetch(url, {
credentials: 'same-origin',
}).catch(err => {
console.error(err)
this.response = err
this._data.message = err.message
throw err
}).then(res => {
this.response = res
if (!res.ok) {
console.error('Fetch not ok:', type, id, res)
return { id }
}
if (type === 'follows') {
return res.text()
} else {
return res.json().catch(err => {
console.error('JSON parsing error:', err)
return { id }
})
}
}).then(json => {
Resource.cache[type][id] = this
this.initialize(json)
if (!this.response.ok) {
console.error('Response status:', this.response.status, this.response.statusText)
console.error('Resource status:', json.status)
}
return this
}))
}).catch(err => {
console.error('Error:', err)
throw err
})
}
static create(opts, force) {
const resource = new this()
return resource.load(opts, force)
}
}
Resource.cache = {}
var xx = 0
export class Manga extends Resource {
get resourceType() {
return xx++ ? 'manga' : 'mangaa'
}
// static API_URL(id = '') { return `/api/manga/${id}` }
static API_URL() { return `/api/` }
initialize(data) {
super.initialize(data.manga)
this.chapters = Object.entries(data.chapter || {}).map(([id, ch]) => { ch.id = parseInt(id); return ch })
this.chapterList = []
}
get id() { return this._data.id }
get title() { return this._data.title || '' }
get langCode() { return this._data.lang_flag }
get langName() { return this._data.lang_name }
get lastChapter() { return this._data.last_chapter }
get isLongStrip() { return this._data.genres && this._data.genres.includes(36) }
get isDoujinshi() { return this._data.genres && this._data.genres.includes(7) }
get isHentai() { return !!this._data.hentai }
get url() {
const title = this.title.toLowerCase().replace(/&[0-9a-z]+;/gi, '').replace(/[^0-9a-z]/gi, ' ').split(' ').filter(t => t).join('-')
return `/title/${this.id}/${title}`
}
get coverUrl() { return `/images/manga/${this.id}.jpg` }
get coverThumbUrl() { return `/images/manga/${this.id}.thumb.jpg` }
getChapterData(id) {
return this.chapters.find(c => c.id === id)
}
getChapterTitle(id, noTitle = false) {
const ch = this.getChapterData(id)
if (!ch) {
return ''
} else {
let title = ''
if (ch.volume)
title += `Vol. ${ch.volume} `
if (ch.chapter)
title += `Ch. ${ch.chapter} `
if (ch.title && !noTitle)
title += `${ch.title}`
if (!title)
title = 'Oneshot'
return title.trim()
}
}
getChapterName(id) {
const ch = this.getChapterData(id)
if (!ch) {
return ''
} else {
if (ch.title)
return ch.title
if (ch.volume && ch.chapter)
return `Vol. ${ch.volume} Ch. ${ch.chapter}`
if (ch.chapter)
return `Ch. ${ch.chapter}`
if (ch.volume)
return `Vol. ${ch.volume}`
return 'Oneshot'
}
}
makeChapterList(lang, [g1 = 0, g2 = 0, g3 = 0]) {
this.chapterList = []
const sameLang = this.chapters.filter(c => c.lang_code === lang)
Manga.sortChapters(sameLang)
let best = null
for (let ch of sameLang) {
if (!best) {
best = ch
} else {
if (!ch.chapter && (!ch.volume || ch.volume === "0") || (best.chapter !== ch.chapter || best.volume !== ch.volume)) {
this.chapterList.push(best)
best = ch
} else if (ch.group_id === g1 && ch.group_id_2 === g2 && ch.group_id_3 === g3) {
best = ch
}
}
}
if (best) {
this.chapterList.push(best)
}
return this.chapterList
}
getAltChapters(id) {
const cur = this.getChapterData(id)
if (!cur) {
return []
} else {
const isNonNumbered = (cur.volume === "" || cur.volume === "0") && cur.chapter === ""
return this.chapters
.filter(c =>
c.lang_code === cur.lang_code
&& c.volume === cur.volume && c.chapter === cur.chapter
&& (!isNonNumbered || cur.title === c.title)
).map(c => new Chapter(c))
}
}
getPrevChapterId(id) {
const index = this.chapterList.findIndex(c => c.id === id)
if (index <= 0) {
return -1
} else {
return this.chapterList[index - 1].id
}
}
getNextChapterId(id) {
const index = this.chapterList.findIndex(c => c.id === id)
if (index === -1 || index === this.chapterList.length - 1) {
return 0
} else {
return this.chapterList[index + 1].id
}
}
areChaptersSequential(id1, id2) {
const c1 = this.getChapterData(id1)
const c2 = this.getChapterData(id2)
if (!c1 || !c2) {
return true
}
const c1Chapter = parseFloat(c1.chapter)
const c2Chapter = parseFloat(c2.chapter)
const c1Volume = parseFloat(c1.volume)
const c2Volume = parseFloat(c2.volume)
if (isNaN(c1Chapter) || isNaN(c2Chapter)) {
return true
} else if (c1Chapter === c2Chapter && c1Volume === c2Volume) {
return true
} else if (Math.abs(c1Chapter - c2Chapter).toFixed(1) <= 1.1) {
return true
} else if ((c1Chapter <= 1 && Math.floor(c1Volume - c2Volume) <= 1) || (c2Chapter <= 1 && Math.floor(c2Volume - c1Volume) <= 1)) {
return true
} else {
return false
}
}
/*areChaptersSequential(c1Chapter, c1Volume, c2Chapter, c2Volume) {
c1Chapter = parseFloat(c1Chapter)
c2Chapter = parseFloat(c2Chapter)
c1Volume = parseFloat(c1Volume)
c2Volume = parseFloat(c2Volume)
if (isNaN(c1Chapter) || isNaN(c2Chapter)) {
return true
} else if (Math.abs(c1Chapter - c2Chapter).toFixed(1) <= 1.1) {
return true
} else if ((c1Chapter === 1 || c2Chapter === 1) && Math.abs(c1Volume - c2Volume) <= 1) {
return true
}
return false
}*/
static sortChapters(chapters) {
const sorter = natsort({ desc: false, insensitive: true })
// sort by volume desc, so that vol null > vol number where ch are equal
Utils.stableSort(chapters, (a, b) => sorter(b.volume, a.volume))
// sort by first group
Utils.stableSort(chapters, (a, b) => sorter(a.group_id, b.group_id))
// sort by chapter number
Utils.stableSort(chapters, (a, b) => sorter(a.chapter, b.chapter))
// add ghost prev vol numbers
let pv = '0'
chapters.forEach(c => {
c.__prev_vol = pv
if (c.volume) {
pv = c.volume
}
})
// sort by vol or prev vol
Utils.stableSort(chapters, (a, b) => sorter(a.volume || a.__prev_vol, b.volume || b.__prev_vol))
// remove ghost vols
chapters.forEach(c => { delete c.__prev_vol })
}
static create(opts, force) {
return super.create(opts, force).then(manga => {
manga._data.id = opts.id
return manga
})
}
}
export class Chapter extends Resource {
get resourceType() { return 'chapter' }
// static API_URL(id = '') { return `/api/chapter/${id}` }
static API_URL() { return `/api/` }
get id() { return this._data.id }
get mangaId() { return this._data.manga_id }
get title() { return this._data.title }
get chapter() { return this._data.chapter }
get volume() { return this._data.volume }
get comments() { return this._data.comments }
get isLastChapter() { return this.manga && this.manga.lastChapter && this.manga.lastChapter !== "0" && this.manga.lastChapter === this.chapter }
get langCode() { return this._data.lang_code }
get langName() { return this._data.lang_name }
get totalPages() { return this._data.page_array ? this._data.page_array.length : 0 }
get groupIds() { return [this._data.group_id, this._data.group_id_2, this._data.group_id_3].filter(n => n) }
get groupNames() { return [this._data.group_name, this._data.group_name_2, this._data.group_name_3].filter(n => n) }
get groupWebsite() { return this._data.group_website }
get timestamp() { return this._data.timestamp }
get prevChapterId() { return this.manga.getPrevChapterId(this.id) }
get nextChapterId() { return this.manga.getNextChapterId(this.id) }
get url() { return `/chapter/${this.id}` }
get externalUrl() { return this._data.external || '' }
get fullTitle() {
let title = ''
if (this.volume) title += `Vol. ${this.volume} `
if (this.chapter) title += `Ch. ${this.chapter} `
if (this.title) title += `${this.title}`
if (!title) title = 'Oneshot'
return title.trim()
}
get isMangaFailed() { try { return !this.manga.response.ok } catch (err) { return true } }
get isNotFound() { try { return this.response.status == 404 } catch (err) { return false } }
get isDelayed() { try { return this.response.status == 409 } catch (err) { return false } }
get isDeleted() { try { return this.response.status == 410 } catch (err) { return false } }
get isRestricted() { try { return this.response.status == 451 } catch (err) { return false } }
get isExternal() { return this._data.status === 'external' }
get message() { return this._data.message }
get isNetworkServer() {
if (!this._isNetworkServer) {
this._isNetworkServer = /mangadex\.network/.test(this._data.server || '')
}
return this._isNetworkServer
}
get pageArray() { return this._data.page_array || [] }
getPage(pg) {
return pg >= 1 && pg <= this.totalPages ? this._data.page_array[pg - 1] : ''
}
get pagesFullURL() { return this.pageArray.map((pg, i) => this.imageURL(i + 1)) }
imageURL(pg) {
return this._data.server + this._data.hash + '/' + this.getPage(pg)
}
makeMangaChapterList() {
this.manga.makeChapterList(this.langCode, this.groupIds)
}
loadManga(force) {
if (!this._data.manga_id) {
console.warn('No manga id for chapter', this.id)
return Promise.resolve(this)
}
return Manga.create({ id: this._data.manga_id }, force).then(manga => {
this.manga = manga
if (!manga.response.ok) {
return Promise.reject(this)
} else {
this.makeMangaChapterList()
return Promise.resolve(this)
}
})
}
static create(opts, force) {
return super.create(opts, force)
.catch(ch => ch.mangaId ? Promise.resolve(ch) : Promise.reject(ch))
.then(ch => {
if (ch._data.page_array) {
ch._data.page_array = ch._data.page_array.filter(p => !!p)
}
return Promise.resolve(ch)
})
.then(ch => ch.loadManga(force))
.catch(ch => ch.manga && !ch.isMangaFailed ? Promise.resolve(ch) : Promise.reject(ch))
}
}
export class Follows extends Resource {
get resourceType() { return 'follows' }
get resourceFormat() { return 'text' }
static API_URL() { return '/follows/' }
get unreadChapters() {
return this.chapters.filter(c => c.id && !c._data.read)
}
get unreadManga() {
return this.unreadChapters.reduce((acc, cur) => {
acc[cur.manga.id] = cur.manga
return acc
}, {})
}
static create(opts, force) {
return super.create(opts, force).then(follows => {
const rows = follows._data.match(/col-md-3 [\s\S]*?chapter-list-group/gim)
if (!rows) {
return []
}
const mangaCache = {}
let mangaTitle = ''
follows.chapters = rows.map(row => {
const none = ['', '']
mangaTitle = (row.match(/manga_title[\s\S]*?title='([\s\S]*?)'/) || none)[1].trim() || ''
if (mangaTitle) { console.log(mangaTitle) }
const mangaId = parseInt((row.match(/data-manga-id="(\d*?)"/) || none)[1])
if (!(mangaId in mangaCache)) {
mangaCache[mangaId] = new Manga()
mangaCache[mangaId].initialize({
manga: {
id: mangaId || 0,
title: mangaTitle,
}
})
}
const manga = mangaCache[mangaId]
const chapter = new Chapter()
chapter.initialize({
id: parseInt((row.match(/data-id="(\d*?)"/) || none)[1]) || null,
title: (row.match(/data-title="([\s\S]*?)"/) || none)[1],
chapter: parseFloat((row.match(/data-chapter="([\d\.]*?)"/) || none)[1]) || null,
volume: parseFloat((row.match(/data-volume="([\d\.]*?)"/) || none)[1]) || null,
timestamp: parseInt((row.match(/data-timestamp="(.*?)"/) || none)[1]) * 1000 || null,
lang_code: (row.match(/flag-(..)/) || none)[1],
read: /chapter_mark_unread_button/.test(row),
})
chapter.manga = manga
manga.chapters.push(chapter)
return chapter
})
return follows
})
}
}
// export default { Manga, Chapter, Follows }