Skip to content

Instantly share code, notes, and snippets.

@ZTRdiamond
Created September 26, 2025 15:37
Show Gist options
  • Select an option

  • Save ZTRdiamond/99730958cb8e8ff4d142b00c0a546669 to your computer and use it in GitHub Desktop.

Select an option

Save ZTRdiamond/99730958cb8e8ff4d142b00c0a546669 to your computer and use it in GitHub Desktop.
Novel Hub Scraper
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