Created
September 26, 2025 15:37
-
-
Save ZTRdiamond/99730958cb8e8ff4d142b00c0a546669 to your computer and use it in GitHub Desktop.
Novel Hub Scraper
This file contains hidden or 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
| import axios, { AxiosInstance } from "axios"; | |
| const api: AxiosInstance = axios.create({ | |
| baseURL: "https://novelhubapp.com/wefeed-h5novel-bff", | |
| timeout: 120000, | |
| headers: { | |
| 'Accept': 'application/json', | |
| 'Origin': 'https://novelhubapp.com', | |
| 'Referer': 'https://novelhub.com/', | |
| 'User-Agent': 'ZanixonGroup/1.0.0', | |
| 'X-Client-Info': '{"timezone":"Asia/Jakarta"}' | |
| } | |
| }); | |
| // typesafe | |
| interface Result { | |
| success: boolean, | |
| result: T, | |
| errors?: string | |
| } | |
| // static type | |
| interface Pager { | |
| hasMore: boolean, | |
| currentPage: number, | |
| nextPage: number, | |
| limitResult: number, | |
| totalCount: bigint | |
| } | |
| interface Cover { | |
| size: bigint, | |
| width: number, | |
| height: number, | |
| ext: string, | |
| url: string | |
| } | |
| interface Novel { | |
| novelId: number; | |
| novelType: number; | |
| title: string; | |
| cover: Cover, | |
| summary: string; | |
| score: string; | |
| totalViews: string; | |
| totalChapters: number; | |
| genres: string[]; | |
| tags: string[]; | |
| totalWords: number; | |
| language: string; | |
| totalWordsFormat: string; | |
| novelStatus: number; | |
| novelStatusDesc: string; | |
| } | |
| interface Home { | |
| id: string, | |
| type?: string; | |
| title?: string; | |
| contentList?: Novel[]; | |
| genreList?: Object[]; | |
| } | |
| interface Chapter { | |
| chapterId: number, | |
| novelId: string, | |
| name: string, | |
| sequel: number, | |
| totalWords: number, | |
| lastUpdateTime: number, | |
| filesize: number, | |
| url: string | |
| } | |
| // dynamic type | |
| // main | |
| class Novelhub { | |
| #api; | |
| constructor(options) { | |
| this.#api = api; | |
| } | |
| async home(): Promise<Result> { | |
| return new Promise(async (resolve, reject) => { | |
| this.#api.get("/home/op-list").then(raw => { | |
| const data = raw.data; | |
| if(!data?.data?.list) return reject({ success: false, errors: "failed retrieve home opening data!" }) | |
| const result: Result = { | |
| success: true, | |
| result: { | |
| items: data?.data?.list.filter(d => ["ContentList", "GenreList"].includes(d.type)).map(d => ({ | |
| id: Number(d.link.replace("/content-list/", "")), | |
| type: d.type, | |
| title: d.title, | |
| contents: d?.contentList?.map(d => ({ | |
| novelId: Number(d.novelId), | |
| novelType: d.novelType, | |
| title: d.title, | |
| cover: { | |
| width: d.cover.width, | |
| height: d.cover.height, | |
| size: d.cover.size, | |
| ext: d.cover.format, | |
| url: d.cover.url | |
| }, | |
| summary: d.summary, | |
| score: d.score, | |
| totalViews: d.totalViews, | |
| totalChapters: d.totalChapters, | |
| genres: d.genres, | |
| tags: d.tags, | |
| totalWords: d.totalWords, | |
| language: d.language, | |
| totalWordsFormat: d.totalWordsFormat, | |
| novelStatus: d.novelStatus, | |
| novelStatusDesc: d.novelStatusDesc, | |
| })), | |
| genres: d?.genreList?.map(d => ({ | |
| genreId: d.genreId, | |
| name: d.name, | |
| cover: { | |
| width: d.cover.width, | |
| height: d.cover.height, | |
| size: d.cover.size, | |
| ext: d.cover.format, | |
| url: d.cover.url | |
| } | |
| })), | |
| })) | |
| } | |
| } | |
| return resolve(result) | |
| }).catch(err => reject({ | |
| success: false, | |
| errors: err.response?.data || err.response || err.message | |
| })) | |
| }); | |
| } | |
| async hotSearch(): Promise<Result> { | |
| return new Promise(async (resolve, reject) => { | |
| this.#api.get("/operation/content-list", { | |
| params: { | |
| opConfig: 52, | |
| page: 1, | |
| perPage: 10 | |
| } | |
| }).then(raw => { | |
| const data = raw.data; | |
| if(!data?.data?.contentList) return reject({ success: false, errors: "failed retrieve hot search data!" }) | |
| const result: Result = { | |
| success: true, | |
| result: { | |
| title: "Hot Search", | |
| items: data?.data?.contentList.map(d => ({ | |
| novelId: Number(d.novelId), | |
| novelType: d.novelType, | |
| title: d.title, | |
| cover: { | |
| width: d.cover.width, | |
| height: d.cover.height, | |
| size: d.cover.size, | |
| ext: d.cover.format, | |
| url: d.cover.url | |
| }, | |
| summary: d.summary, | |
| score: d.score, | |
| totalViews: d.totalViews, | |
| totalChapters: d.totalChapters, | |
| genres: d.genres, | |
| tags: d.tags, | |
| totalWords: d.totalWords, | |
| language: d.language, | |
| totalWordsFormat: d.totalWordsFormat, | |
| novelStatus: d.novelStatus, | |
| novelStatusDesc: d.novelStatusDesc | |
| })) | |
| } | |
| } | |
| return resolve(result) | |
| }).catch(err => reject({ | |
| success: false, | |
| errors: err.response?.data || err.response || err.message | |
| })) | |
| }); | |
| } | |
| async search(keyword: string, page: number = 1, limit: number = 20): Promise<Result> { | |
| return new Promise(async (resolve, reject) => { | |
| if(!keyword) return reject({ | |
| success: false, | |
| errors: "missing keyword input" | |
| }); | |
| this.#api.get("/web/novel/search", { | |
| params: { | |
| keyword, | |
| page: 1, | |
| perPage: 50 | |
| } | |
| }).then(raw => { | |
| const data = raw.data; | |
| if(!data?.data?.list) return reject({ success: false, errors: "failed retrieve hot search data!" }) | |
| const result: Result = { | |
| success: true, | |
| result: { | |
| items: data?.data?.list.map(d => ({ | |
| novelId: Number(d.novelId), | |
| novelType: d.novelType, | |
| title: d.title, | |
| cover: { | |
| width: d.cover.width, | |
| height: d.cover.height, | |
| size: d.cover.size, | |
| ext: d.cover.format, | |
| url: d.cover.url | |
| }, | |
| summary: d.summary, | |
| score: d.score, | |
| totalViews: d.totalViews, | |
| totalChapters: d.totalChapters, | |
| genres: d.genres, | |
| tags: d.tags, | |
| totalWords: d.totalWords, | |
| language: d.language, | |
| totalWordsFormat: d.totalWordsFormat, | |
| novelStatus: d.novelStatus, | |
| novelStatusDesc: d.novelStatusDesc | |
| })) | |
| } | |
| } | |
| return resolve(result) | |
| }).catch(err => reject({ | |
| success: false, | |
| errors: err.response?.data || err.response || err.message | |
| })) | |
| }); | |
| } | |
| async genre(genreId: number = 15, page: number = 1, limit: number = 20): Promise<Result> { | |
| return new Promise(async (resolve, reject) => { | |
| this.#api.get("/web/novel/genre-content-list", { | |
| params: { | |
| genreId, | |
| opConfId: 3, | |
| novelStatus: 0, | |
| orderType: 1, | |
| page: 1, | |
| perPage: 50 | |
| } | |
| }).then(raw => { | |
| const data = raw.data; | |
| if(!data?.data?.list) return reject({ success: false, errors: "failed retrieve hot search data!" }) | |
| const result: Result = { | |
| success: true, | |
| result: { | |
| genres: data?.data?.genres.sort((a, b) => Number(a.genreId) - Number(b.genreId)).map(d => ({ | |
| genreId: d.genreId, | |
| name: d.name | |
| })), | |
| items: data?.data?.list.map(d => ({ | |
| novelId: Number(d.novelId), | |
| novelType: d.novelType, | |
| title: d.title, | |
| cover: { | |
| width: d.cover.width, | |
| height: d.cover.height, | |
| size: d.cover.size, | |
| ext: d.cover.format, | |
| url: d.cover.url | |
| }, | |
| summary: d.summary, | |
| score: d.score, | |
| totalViews: d.totalViews, | |
| totalChapters: d.totalChapters, | |
| genres: d.genres, | |
| tags: d.tags, | |
| totalWords: d.totalWords, | |
| language: d.language, | |
| totalWordsFormat: d.totalWordsFormat, | |
| novelStatus: d.novelStatus, | |
| novelStatusDesc: d.novelStatusDesc | |
| })) | |
| } | |
| } | |
| return resolve(result) | |
| }).catch(err => reject({ | |
| success: false, | |
| errors: err.response?.data || err.response || err.message | |
| })) | |
| }); | |
| } | |
| async chapters(novelId: string, page: number = 1, limit: number = 20): Promise<Result> { | |
| return new Promise(async (resolve, reject) => { | |
| if(!novelId) return reject({ | |
| success: false, | |
| errors: "missing novelId input!" | |
| }) | |
| this.#api.get("/web/novel/chapter-list", { | |
| params: { | |
| novelId, | |
| order: "ASC" | |
| } | |
| }).then(raw => { | |
| const data = raw.data; | |
| if(!data?.data?.chapterList) return reject({ success: false, errors: "failed retrieve hot search data!" }) | |
| const result: Result = { | |
| success: true, | |
| result: { | |
| chapters: data?.data?.chapterList.map(d => ({ | |
| chapterId: Number(d.chapterId), | |
| novelId: Number(d.novelId), | |
| name: d.chapterName, | |
| sequel: d.seq, | |
| totalWords: d.totalWords, | |
| lastUpdateTime: Number(d.lastUpdateTime), | |
| filesize: d.filesize, | |
| url: d.fileUrl | |
| })) | |
| } | |
| } | |
| return resolve(result) | |
| }).catch(err => reject({ | |
| success: false, | |
| errors: err.response?.data || err.response || err.message | |
| })) | |
| }); | |
| } | |
| } | |
| export default new Novelhub(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment