forked from manga-download/hakuneko
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from MikeZeDev/Experiments
Experiments
- Loading branch information
Showing
15 changed files
with
1,206 additions
and
10 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 |
---|---|---|
|
@@ -13,4 +13,5 @@ cache | |
/redist/*.zip | ||
/test/*.log | ||
junit.xml | ||
.idea/ | ||
.idea/ | ||
.vs |
Binary file not shown.
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,53 @@ | ||
import WordPressMadara from './templates/WordPressMadara.mjs'; | ||
export default class BeautyManga extends WordPressMadara { | ||
constructor() { | ||
super(); | ||
super.id = 'beautymanga'; | ||
super.label = 'BeautyManga'; | ||
this.tags = [ 'manga', 'webtoon', 'english' ]; | ||
this.url = 'https://mangadex.today'; | ||
this.path = '/popular-manga'; | ||
this.queryMangas = 'div.item-thumb.hover-details.c-image-hover a'; | ||
this.queryMangasPageCount = 'ul.pagination li:nth-last-of-type(2) a'; | ||
this.pathMangas = '?page=%PAGE%'; | ||
this.queryPages = 'p#arraydata'; | ||
this.mangaTitleFilter = ''; | ||
} | ||
async _getMangasFromPage(page) { | ||
let uri = new URL(this.path + this.pathMangas.replace('%PAGE%', page), this.url); | ||
uri.pathname = uri.pathname.replace(/\/+/g, '/'); | ||
let request = new Request(uri, this.requestOptions); | ||
let data = await this.fetchDOM(request, this.queryMangas); | ||
return data.map(element => { | ||
return { | ||
id: new URL(element.href, request.url).pathname, | ||
title: element.title.replace(this.mangaTitleFilter, '').trim() | ||
}; | ||
}); | ||
} | ||
async _getChaptersAjaxOld(mangaID) { | ||
const uri = new URL('/ajax-list-chapter?mangaID='+mangaID, this.url); | ||
const request = new Request(uri, { | ||
method: 'GET' | ||
}); | ||
const data = await this.fetchDOM(request, this.queryChapters); | ||
if (data.length) { | ||
return data; | ||
} else { | ||
throw new Error('No chapters found (new ajax endpoint)!'); | ||
} | ||
} | ||
async _getPages(chapter) { | ||
let uri = new URL(chapter.id, this.url); | ||
let request = new Request(uri, this.requestOptions); | ||
let data = await this.fetchDOM(request, this.queryPages); | ||
let el = data[0].innerText.split(','); | ||
return el.map(element => { | ||
const uri = new URL(this.getAbsolutePath(element, request.url)); | ||
return this.createConnectorURI({ | ||
url: uri.href, | ||
referer: uri.origin | ||
}); | ||
}); | ||
} | ||
} |
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,154 @@ | ||
import Connector from '../engine/Connector.mjs'; | ||
import Manga from '../engine/Manga.mjs'; | ||
|
||
export default class BookWalker extends Connector { | ||
|
||
constructor() { | ||
super(); | ||
super.id = 'bookwalker'; | ||
super.label = 'BookWalker'; | ||
this.tags = [ 'manga', 'japanese' ]; | ||
this.url = 'https://global.bookwalker.jp'; | ||
} | ||
|
||
async _getMangaFromURI(uri) { | ||
let id = uri.pathname; | ||
let request = new Request(uri, this.requestOptions); | ||
let data = await this.fetchDOM(request, 'div.detail-book-title h1[itemprop="name"]'); | ||
let title = data[0].textContent.trim(); | ||
return new Manga(this, id, title); | ||
} | ||
|
||
async _getChapters(manga) { | ||
const url = new URL(manga.id, this.url); | ||
url.searchParams.set('sample', '2'); | ||
return [{id : url.pathname+url.search, title : 'Free Sample',}]; | ||
} | ||
|
||
async _getPages(chapter) { | ||
const script = ` | ||
new Promise((resolve, reject) => { | ||
setTimeout(async () => { | ||
const a2f = NFBR.a2F ? new NFBR.a2F() : new NFBR.a2f(); | ||
const params = new URL(window.location).searchParams; | ||
const parameters = await a2f.a5W({ | ||
contentId: params.get(NFBR.a5q.Key.CONTENT_ID), // Content ID => 'cid' | ||
a6m: params.get(NFBR.a5q.Key.LICENSE_TOKEN), // License Token => 'lit' | ||
preview: params.get(NFBR.a5q.Key.LOOK_INSIDE) === '1', // Look Inside => 'lin' | ||
contentType: params.get(NFBR.a5q.Key.CONTENT_TYPE) || 1, // Content Type => 'cty' | ||
title: params.get(NFBR.a5q.Key.CONTENT_TITLE), // Content Title => 'cti' | ||
winWidth: 3840, | ||
winHeight: 2160 | ||
}); | ||
//Create a model | ||
const model = new NFBR.a6G.Model({ | ||
'settings': new self.NFBR.Settings('NFBR.SettingData'), | ||
'viewerFontSize': self.NFBR.a0X.a3k, | ||
'viewerFontFace': self.NFBR.a0X.a3k, | ||
'viewerSpreadDouble': true, | ||
'viewerb5c': null, | ||
'viewerSpread': {}, | ||
'queryParamForContentUrl' : parameters.contentAppendParam, | ||
}); | ||
//Create the bookloader | ||
const bl = new NFBR.a5n(); | ||
bl.B0a = "normal_default"; | ||
//Create a "Content" that will be filled using the bookloader as5 async function | ||
let data = new NFBR.a6i.Content(parameters.url); | ||
let v_a6L = new NFBR.a6G.a6L(model); | ||
await bl.a5s(data, "configuration", v_a6L); | ||
//console.log(data); | ||
//data is now our JSON with all the infos | ||
const pages = data.configuration.contents.map((page, index) => { | ||
let mode = 'raw'; | ||
let extension = '.jpeg'; | ||
//*****************/ | ||
//GETTING PAGE URL | ||
//*****************/ | ||
//Create a Page | ||
const fPage = new NFBR.a6i.Page(index, page.file, "0", extension, ""); | ||
const realURL = v_a6L.getPageContentUrl_(data, fPage); | ||
//*****************************/ | ||
//GETTING IMAGE SCRAMBLE DATA | ||
//*****************************/ | ||
//Fill infos in the page for a7b to work | ||
const fileinfos = data.files[index].FileLinkInfo.PageLinkInfoList[0].Page; | ||
fPage.width = fileinfos.Size.Width; | ||
fPage.height = fileinfos.Size.Height; | ||
fPage.info = fileinfos; | ||
//Fill more infos needed for unscrambling | ||
fPage.A3j(data); | ||
let blocks = []; | ||
if (fileinfos.BlockHeight) //if we have a block size for the page, its a puzzle ! | ||
{ | ||
mode = 'puzzle'; | ||
blocks = window.NFBR.a6G.a5x.prototype.R8x(fPage, fPage.width, fPage.height) | ||
} | ||
return { | ||
mode: mode, | ||
imageUrl: realURL, | ||
encryption: { | ||
blocks: JSON.stringify(blocks),//stringify the array greatly speed up createConnectorURI | ||
} | ||
}; | ||
}); | ||
resolve(pages); | ||
}, 1000); | ||
}); | ||
`; | ||
const uri = new URL( chapter.id, this.url ); | ||
const request = new Request( uri.href, this.requestOptions ); | ||
const data = await Engine.Request.fetchUI(request, script); | ||
return data.map(page => page.mode == 'raw' ? page.imageUrl : this.createConnectorURI(page)); | ||
} | ||
|
||
async _handleConnectorURI(payload) { | ||
const uri = new URL(payload.imageUrl, this.url); | ||
const request = new Request(uri, this.requestOptions); | ||
const response = await fetch(request); | ||
switch (payload.mode) { | ||
case 'puzzle': { | ||
let data = await response.blob(); | ||
data = await this._descrambleImage(data, payload.encryption.blocks); | ||
return this._blobToBuffer(data); | ||
} | ||
default: { | ||
let data = await response.blob(); | ||
return this._blobToBuffer(data); | ||
} | ||
} | ||
} | ||
|
||
async _descrambleImage(scrambled, blocks) { | ||
let bitmap = await createImageBitmap(scrambled); | ||
return new Promise(resolve => { | ||
let canvas = document.createElement('canvas'); | ||
canvas.width = bitmap.width; | ||
canvas.height = bitmap.height; | ||
var ctx = canvas.getContext('2d'); | ||
const blockz = JSON.parse(blocks); | ||
for (let q of blockz) { | ||
ctx.drawImage(bitmap, q.destX, q.destY, q.width, q.height, q.srcX, q.srcY, q.width, q.height); | ||
} | ||
canvas.toBlob(data => { | ||
resolve(data); | ||
}, Engine.Settings.recompressionFormat.value, parseFloat(Engine.Settings.recompressionQuality.value)/100); | ||
} ); | ||
} | ||
|
||
} |
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,135 @@ | ||
import Connector from '../engine/Connector.mjs'; | ||
import Manga from '../engine/Manga.mjs'; | ||
|
||
export default class CuuTruyen extends Connector { | ||
constructor() { | ||
super(); | ||
super.id = 'cuutruyen'; | ||
super.label = 'Cứu Truyện'; | ||
this.tags = ['manga', 'vietnamese']; | ||
this.url = 'https://cuutruyen.net'; | ||
} | ||
|
||
async _getMangaFromURI(uri) { | ||
const mangaid = uri.href.match(/\/mangas\/([0-9]+)/)[1]; | ||
const req = new URL(`/api/v2/mangas/${mangaid}`, this.url); | ||
const request = new Request(req, this.requestOptions); | ||
const { data: { name } } = await this.fetchJSON(request); | ||
return new Manga(this, mangaid, name.trim()); | ||
} | ||
|
||
async _getMangas() { | ||
const uri = new URL('/api/v2/mangas/recently_updated?page=1&per_page=30', this.url); | ||
const request = new Request(uri, this.requestOptions); | ||
const data = await this.fetchJSON(request); | ||
const pages = data._metadata.total_pages; | ||
|
||
const mangaList = this._getMangasFromPage(data); | ||
|
||
for (let page = 2; page <= pages; page++) { | ||
const uri = new URL(`/api/v2/mangas/recently_updated?page=${page}&per_page=30`, this.url); | ||
const request = new Request(uri, this.requestOptions); | ||
const data = await this.fetchJSON(request); | ||
const mangas = this._getMangasFromPage(data); | ||
mangaList.push(...mangas); | ||
} | ||
return mangaList; | ||
} | ||
|
||
_getMangasFromPage(data) { | ||
return data.data.map((c) => ({ | ||
id: c.id, | ||
title: c.name.trim(), | ||
})); | ||
} | ||
|
||
async _getChapters(manga) { | ||
const uri = new URL(`/api/v2/mangas/${manga.id}/chapters`, this.url); | ||
const request = new Request(uri, this.requestOptions); | ||
const { data } = await this.fetchJSON(request); | ||
return data | ||
.filter((chapter) => chapter.status === 'processed') | ||
.map((chapter) => { | ||
let title = `Chapter ${chapter.number}`; | ||
|
||
if (chapter.name) { | ||
title += `: ${chapter.name}`; | ||
} | ||
|
||
return { id: chapter.id, title }; | ||
}); | ||
} | ||
|
||
async _getPages(chapter) { | ||
const uri = new URL('/api/v2/chapters/' + chapter.id, this.url); | ||
const request = new Request(uri, this.requestOptions); | ||
const { data: { pages } } = await this.fetchJSON(request); | ||
|
||
if (pages.some((image) => image.status !== 'processed')) { | ||
throw new Error('This chapter is still processing, please try again later.'); | ||
} | ||
|
||
return pages.map((image) => { | ||
return this.createConnectorURI({ | ||
url: image.image_url, | ||
drmData: image.drm_data, | ||
}); | ||
}); | ||
} | ||
|
||
async _handleConnectorURI(payload) { | ||
const response = await fetch(payload.url, { | ||
cache: 'no-cache', | ||
referrer: `${this.url}/`, | ||
headers: { | ||
'accept': 'image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5', | ||
}, | ||
}); | ||
|
||
if (!payload.drmData) { | ||
return this._blobToBuffer(await response.blob()); | ||
} | ||
|
||
const decryptedDrmData = this.decodeXorCipher(atob(payload.drmData), '3141592653589793'); | ||
|
||
if (!decryptedDrmData.startsWith('#v4|')) { | ||
throw new Error(`Invalid DRM data (does not start with magic bytes): ${decryptedDrmData}`); | ||
} | ||
|
||
const image = await createImageBitmap(await response.blob()); | ||
const canvas = document.createElement('canvas'); | ||
|
||
canvas.width = image.width; | ||
canvas.height = image.height; | ||
|
||
const ctx = canvas.getContext('2d'); | ||
let sy = 0; | ||
|
||
for (const t of decryptedDrmData.split('|').slice(1)) { | ||
const [dy, height] = t.split('-', 2).map(Number); | ||
|
||
ctx.drawImage(image, 0, sy, image.width, height, 0, dy, image.width, height); | ||
sy += height; | ||
} | ||
|
||
return this._blobToBuffer(await this._canvasToBlob(canvas)); | ||
} | ||
|
||
_canvasToBlob(canvas) { | ||
return new Promise(resolve => { | ||
canvas.toBlob(data => { | ||
resolve(data); | ||
}, Engine.Settings.recompressionFormat.value, parseFloat(Engine.Settings.recompressionQuality.value) / 100); | ||
}); | ||
} | ||
|
||
decodeXorCipher(data, key) { | ||
let output = ""; | ||
|
||
for (let i = 0; i < data.length; i++) { | ||
output += String.fromCharCode(data.charCodeAt(i) ^ key.charCodeAt(i % key.length)); | ||
} | ||
|
||
return output; | ||
} | ||
} |
Oops, something went wrong.