mirror of
https://github.com/holo-gfx/mangadex.git
synced 2024-11-25 02:38:22 -05:00
418 lines
13 KiB
JavaScript
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 }
|