@yeci226/nhentai-ts
Version:
A scraper for NHentai with types
299 lines (283 loc) • 10.1 kB
text/typescript
import axios, { AxiosInstance } from 'axios'
import { load } from 'cheerio'
import { CookieJar } from 'tough-cookie'
import { HttpsCookieAgent } from 'http-cookie-agent/http'
import { parseDoujinList, parseDoujinInfo } from '../../Parser'
import { sites } from '../constants'
import { IDoujinInfo, TSite, IList } from '../../Types'
import { getAPIGalleryPages } from '../util'
export class NHentai {
#axios: AxiosInstance
/**
* Helper method to extract site name from URL
*/
private getSiteName(): 'to' | 'net' | 'website' {
const siteName = this._options.site
.replace('https://', '')
.replace('http://', '')
.replace(/\/$/, '')
if (siteName.includes('nhentai.to')) return 'to'
if (siteName.includes('nhentai.net')) return 'net'
if (siteName.includes('nhentai.website')) return 'website'
// Default fallback
return 'to'
}
/**
* Constructs an instance of the NHentai class
* @param _options Options of the NHentai class
*/
constructor(
private readonly _options: {
site: TSite | `https://${TSite}`
user_agent?: string
cookie_value?: string
} = {
site: 'https://nhentai.to'
}
) {
this.#axios = axios
// Extract the site name from the URL
const siteName = this._options.site
.replace('https://', '')
.replace('http://', '')
.replace(/\/$/, '') as TSite
if (!sites.includes(siteName)) {
this._options.site = 'https://nhentai.to'
}
if (!this._options.site.startsWith('https://'))
this._options.site =
`https://${this._options.site}` as `https://${TSite}`
if (
this._options.site.includes('nhentai.net') &&
(!this._options.cookie_value || !this._options.user_agent)
)
throw new Error(
`Assign the ${
!this._options.cookie_value
? "'cookie_value'"
: "'user_agent'"
} in the instance of the class to use this site.`
)
if (this._options.cookie_value) {
const jar = new CookieJar()
jar.setCookie(this._options.cookie_value, this._options.site)
const httpsAgent = new HttpsCookieAgent({ cookies: { jar } })
this.#axios = axios.create({ httpsAgent })
}
if (this._options.user_agent)
this.#axios.defaults.headers.common['User-Agent'] =
this._options.user_agent
}
/**
* Gets a random doujin
* @returns Info of the random doujin
*/
public getRandom = async (): Promise<IDoujinInfo> =>
await this.#axios
.get<string>(`${this._options.site}/random`)
.then(async ({ data }) =>
parseDoujinInfo(
load(data),
this.getSiteName(),
this._options.site.includes('net')
? await getAPIGalleryPages(this.#axios, data)
: undefined
)
)
.catch((err) => {
throw new Error(err.message)
})
/**
* Explores the list of doujin
* @param page Page number of the list
* @returns The doujin list
*/
public explore = async (page = 1): Promise<IList> => {
if (isNaN(page) || page < 1) page = 1
return await this.#axios
.get<string>(`${this._options.site}?page=${page}`)
.then(({ data }) => parseDoujinList(load(data), this.getSiteName()))
.catch((err) => {
throw new Error(err.message)
})
}
/**
* Searches for a doujin by a query
* @param query Query of the doujin to search
* @param options Options for searching
* @returns The result of the search
*/
public search = async (
query: string,
options?: { page?: number }
): Promise<IList> => {
if (!query)
throw new Error("The 'query' parameter shouldn't be undefined")
let page = 1
if (options?.page && options.page > 0) page = options.page
return await this.#axios
.get<string>(`${this._options.site}/search?q=${query}&page=${page}`)
.then((res) => {
const results = parseDoujinList(
load(res.data),
this.getSiteName()
)
if (!results.data.length)
throw new Error('No search results found')
return results
})
}
/**
* Searches Tag for a doujin by a query
* @param query Tag of the doujin to search
* @param options Options for searching
* @returns The result of the search
*/
public searchWithTag = async (
query: string,
options?: { page?: number }
): Promise<IList> => {
if (!query)
throw new Error("The 'query' parameter shouldn't be undefined")
let page = 1
if (options?.page && options.page > 0) page = options.page
return await this.#axios
.get<string>(
`${this._options.site}/tag/${query}${
page > 1 ? `?page=${page}` : ''
}`
)
.then((res) => {
const results = parseDoujinList(
load(res.data),
this.getSiteName()
)
if (!results.data.length)
throw new Error('No tags results found')
return results
})
}
/**
* Searches artist for a doujin by a query
* @param query Artist of the doujin to search
* @param options Options for searching
* @returns The result of the search
*/
public searchWithArtist = async (
query: string,
options?: { page?: number }
): Promise<IList> => {
if (!query)
throw new Error("The 'query' parameter shouldn't be undefined")
let page = 1
if (options?.page && options.page > 0) page = options.page
return await this.#axios
.get<string>(
`${this._options.site}/artist/${query}${
page > 1 ? `?page=${page}` : ''
}`
)
.then((res) => {
const results = parseDoujinList(
load(res.data),
this.getSiteName()
)
if (!results.data.length)
throw new Error('No artists results found')
return results
})
}
/**
* Searches parody for a doujin by a query
* @param query Parody of the doujin to search
* @param options Options for searching
* @returns The result of the search
*/
public searchWithParody = async (
query: string,
options?: { page?: number }
): Promise<IList> => {
if (!query)
throw new Error("The 'query' parameter shouldn't be undefined")
let page = 1
if (options?.page && options.page > 0) page = options.page
return await this.#axios
.get<string>(
`${this._options.site}/parody/${query}${
page > 1 ? `?page=${page}` : ''
}`
)
.then((res) => {
const results = parseDoujinList(
load(res.data),
this.getSiteName()
)
if (!results.data.length)
throw new Error('No parodies results found')
return results
})
}
/**
* Searches character for a doujin by a query
* @param query Character of the doujin to search
* @param options Options for searching
* @returns The result of the search
*/
public searchWithCharacter = async (
query: string,
options?: { page?: number }
): Promise<IList> => {
if (!query)
throw new Error("The 'query' parameter shouldn't be undefined")
let page = 1
if (options?.page && options.page > 0) page = options.page
return await this.#axios
.get<string>(
`${this._options.site}/character/${query}${
page > 1 ? `?page=${page}` : ''
}`
)
.then((res) => {
const results = parseDoujinList(
load(res.data),
this.getSiteName()
)
if (!results.data.length)
throw new Error('No characters results found')
return results
})
}
/**
* Gets the info of a doujin by its ID
* @param id ID of the doujin
* @returns Info of the doujin
*/
public getDoujin = async (id: string | number): Promise<IDoujinInfo> => {
if (!id) throw new Error("The 'id' parameter shouldn't be undefined")
const valid = await this.validate(id)
if (!valid) throw new Error('Invalid doujin ID')
return await this.#axios
.get(`${this._options.site}/g/${id}`)
.then(async (res) =>
parseDoujinInfo(
load(res.data),
this.getSiteName(),
this._options.site.includes('net')
? await getAPIGalleryPages(this.#axios, res.data)
: undefined
)
)
.catch((err) => {
throw new Error(err.message)
})
}
/**
* Validates the ID of a doujin
* @param id ID of the doujin to check
*/
public validate = (id: string | number): Promise<boolean> =>
this.#axios
.get(`${this._options.site}/g/${id}`)
.then(() => true)
.catch(() => false)
}