Skip to content

Commit

Permalink
Release 1.0.2
Browse files Browse the repository at this point in the history
  • Loading branch information
LuckyYam authored Feb 25, 2024
2 parents 0eb3958 + d3febf5 commit c4ae9db
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 88 deletions.
32 changes: 28 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![NPM](https://img.shields.io/badge/Available%20On-NPM-lightgrey.svg?logo=npm&logoColor=339933&labelColor=white&style=flat-square)](https://www.npmjs.com/package/@shineiichijo/nhentai-ts)

Scrap and build a PDF of a doujin from NHentai (only mirror sites, [check the available sites here](https://github.com/LuckyYam/nhentai-ts/blob/master/src/lib/constants.ts#L1)).
Scrap and build a PDF of a doujin from NHentai. [Check the available sites here](https://github.com/LuckyYam/nhentai-ts/blob/master/src/lib/constants.ts#L1).

[Documentation](https://luckyyam.github.io/nhentai-ts/)

Expand All @@ -13,10 +13,34 @@ Scrap and build a PDF of a doujin from NHentai (only mirror sites, [check the av
yarn add @shineiichijo/nhentai-ts
```

## Note

If you're choosing `nhentai.net` for the site, make sure to follow the following steps (as the site has enabled cloudflare protection):

- Open https://nhentai.net/ in your browser.
- Open Dev Tools and set the User Agent to what you want (you'll need the user agent).
- Reload the site and wait for the clearance of cloudflare (without closing the Dev Tools).
- Save the cookie value from the Network Tab (`cf_clearance` value).

After following all these steps, you are all set. You can also check the [example](#usage-examples) of it (at the first one). Remember, the cookie value expires after 30 minutes of inactivity. So, you might have to do the above steps again (in case you're gonna use it again).

## Usage Examples
```ts
import { NHentai } from '@shineiichijo/nhentai-ts'

const user_agent = 'User Agent'
const cookie_value = 'cookie'
const nhentai = new NHentai({ site: 'nhentai.net', user_agent, cookie_value }) //check above
;(async () => {
//Explores the home page
const { data } = await nhentai.explore()
console.log(data)
})()
```

```ts
import { NHentai } from '@shineiichijo/nhentai-ts'

const nhentai = new NHentai()
;(async () => {
//searches for manga
Expand All @@ -33,16 +57,16 @@ const nhentai = new NHentai()
```ts
import { NHentai } from '@shineiichijo/nhentai-ts'

const nhentai = new NHentai({ site: 'nhentai.website' }) //configuring a mirror site of the class (you can check the available sites here: https://github.com/LuckyYam/nhentai-ts/blob/master/src/lib/constants.ts#L1)
const nhentai = new NHentai({ site: 'nhentai.to' }) //configuring a mirror site of the class (you can check the available sites here: https://github.com/LuckyYam/nhentai-ts/blob/master/src/lib/constants.ts#L1)
//validates the ID of a doujin
nhentai.validate('172').then(console.log)
```

```ts
import { NHentai } from '@shineiichijo/nhentai-ts'

const nhentai = new NHentai({ site: 'https://nhentai.xxx' })
//explores all of the available doujin
const nhentai = new NHentai()
//explores all of the available doujin (homepage)
nhentai.explore(2 /* Page number of exploring the doujin */).then(console.log)
```

Expand Down
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@shineiichijo/nhentai-ts",
"description": "A scraper for NHentai with types",
"version": "1.0.1",
"version": "1.0.2",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"license": "MIT",
Expand Down Expand Up @@ -38,14 +38,17 @@
"docs": "yarn typedoc"
},
"dependencies": {
"axios": "^0.27.2",
"axios": "^1.6.7",
"cheerio": "^1.0.0-rc.12",
"http-cookie-agent": "^6.0.1",
"jszip": "^3.10.0",
"pdfkit": "^0.13.0"
"pdfkit": "^0.13.0",
"tough-cookie": "^4.1.3"
},
"devDependencies": {
"@types/node": "^18.6.1",
"@types/pdfkit": "^0.12.6",
"@types/tough-cookie": "^4.0.5",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
"eslint": "^8.20.0",
Expand Down
50 changes: 33 additions & 17 deletions src/Parser/doujin.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import { CheerioAPI } from 'cheerio'
import { baseURLS, imageSites, Pages } from '../lib'
import { baseURLS, clean, getExtension, Pages, imageSites } from '../lib'
import { TURL, IDoujinInfo } from '../Types'

export const parseDoujinInfo = (
$: CheerioAPI,
site: keyof typeof baseURLS
site: keyof typeof baseURLS,
api_pages?: { t: string }[]
): IDoujinInfo => {
const pages: string[] = []
$('.thumb-container').each((i, el) => {
const url = $(el).find('a > img').attr('data-src')
if (url)
const gallery_id = (
$('.thumb-container').first().find('a > img').attr('data-src') ||
'/galleries/'
).split('/galleries/')[0]
if (site === 'net' && api_pages)
api_pages.forEach((page, i) =>
pages.push(
url
.replace(`${i + 1}t`, `${i + 1}`)
.replace(imageSites[site], 'i.nhentai.net')
`https://i.nhentai.net/galleries/${gallery_id}/${
i + 1
}.${getExtension(page.t)}`
)
})
)
else
$('.thumb-container').each((i, el) => {
const url = $(el).find('a > img').attr('data-src')
if (url)
pages.push(
url
.replace(`${i + 1}t`, `${i + 1}`)
.replace(imageSites[site], 'i.nhentai.net')
)
})
const cover =
$('#cover').find('a > img').attr('data-src') ||
$('#cover').find('a > img').attr('src')
Expand Down Expand Up @@ -67,14 +81,16 @@ export const parseDoujinInfo = (
id,
title: titles.english,
originalTitle: titles.original,
parodies,
characters,
tags,
artists,
groups,
languages,
categories,
cover: cover ? cover.replace('cdn.dogehls.xyz', 't3.nhentai.net') : null,
parodies: clean(parodies),
characters: clean(characters),
tags: clean(tags),
artists: clean(artists),
groups: clean(groups),
languages: clean(languages),
categories: clean(categories),
cover: cover
? cover.replace('cdn.dogehls.xyz', 't3.nhentai.net')
: null,
images,
url
}
Expand Down
38 changes: 15 additions & 23 deletions src/Parser/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,26 @@ export const parseDoujinList = (
): IList => {
const data: IList['data'] = []
const baseURL = baseURLS[site]
let currentPage = 1
let totalPages = 1
const pageElements =
site === 'to'
? $('body > section')
: site === 'xxx'
? $('#content > section')
: $('#fnh > section')
if (pageElements.find('.page.current').html()) {
currentPage = Number(pageElements.find('.page.current').text())
const split = $(pageElements)
.last()
.text()
.split('\n')
.filter((el) => Number(el) >= 1)
totalPages = Number(split[split.length - 1])
}
const pagination = {
currentPage,
hasNextPage: totalPages > currentPage,
totalPages
}
const currentPage = Number($('.pagination').find('.page.current').text())
const totalPages = Number(
($('.pagination').find('a.last').attr('href') || '').split('page=')[1]
)
const pagination =
currentPage === 0
? null
: {
currentPage,
hasNextPage: totalPages > currentPage,
totalPages
}
$('.gallery').each((i, el) => {
const contentElements = $(el).find('a')
const slug = contentElements.attr('href')
const id = slug ? slug.split('g/')[1].replace('/', '') : ''
const url = `${baseURL}/g/${id}` as TURL
const coverSlug = contentElements.find('img').attr('data-src')
const coverSlug =
contentElements.find('a > img').attr('data-src') ||
contentElements.find('a > img').attr('src')
const cover = coverSlug
? `${
coverSlug.startsWith('/galleries/')
Expand Down
2 changes: 1 addition & 1 deletion src/Types/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { List } from '../lib/Classes/List'

export interface IList {
/** Pagination of the doujin list */
pagination: IPagination
pagination: IPagination | null
/** Data of the list */
data: List[]
}
Expand Down
83 changes: 59 additions & 24 deletions src/lib/Classes/NHentai.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,75 @@
import axios from 'axios'
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
/**
* Constructs an instance of the NHentai class
* @param options Options of the NHentai class
* @param _options Options of the NHentai class
*/
constructor(
private options: { site: TSite | `https://${TSite}` } = {
private readonly _options: {
site: TSite | `https://${TSite}`
user_agent?: string
cookie_value?: string
} = {
site: 'https://nhentai.to'
}
) {
this.#axios = axios
if (
!sites.includes(
this.options.site
this._options.site
.replace('https:', '')
.replace(/\//g, '') as TSite
)
)
this.options.site = 'https://nhentai.website'
if (!this.options.site.startsWith('https://'))
this.options.site =
`https://${this.options.site}` as `https://${TSite}`
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 axios
.get<string>(`${this.options.site}/random`)
.then(({ data }) =>
await this.#axios
.get<string>(`${this._options.site}/random`)
.then(async ({ data }) =>
parseDoujinInfo(
load(data),
this.options.site.split('nhentai.')[1] as 'to'
this._options.site.split('nhentai.')[1] as 'to',
this._options.site.includes('net')
? await getAPIGalleryPages(this.#axios, data)
: undefined
)
)
.catch((err) => {
Expand All @@ -51,12 +83,12 @@ export class NHentai {
*/
public explore = async (page: number = 1): Promise<IList> => {
if (isNaN(page) || page < 1) page = 1
return await axios
.get<string>(`${this.options.site}?page=${page}`)
return await this.#axios
.get<string>(`${this._options.site}?page=${page}`)
.then(({ data }) =>
parseDoujinList(
load(data),
this.options.site.split('nhentai.')[1] as 'to'
this._options.site.split('nhentai.')[1] as 'to'
)
)
.catch((err) => {
Expand All @@ -78,12 +110,12 @@ export class NHentai {
throw new Error("The 'query' parameter shouldn't be undefined")
let page = 1
if (options?.page && options.page > 0) page = options.page
return await axios
.get<string>(`${this.options.site}/search?q=${query}&page=${page}`)
return await this.#axios
.get<string>(`${this._options.site}/search?q=${query}&page=${page}`)
.then((res) => {
const results = parseDoujinList(
load(res.data),
this.options.site.split('nhentai.')[1] as 'to'
this._options.site.split('nhentai.')[1] as 'to'
)
if (!results.data.length)
throw new Error('No doujin results found')
Expand All @@ -100,12 +132,15 @@ export class NHentai {
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 axios
.get(`${this.options.site}/g/${id}`)
.then((res) =>
return await this.#axios
.get(`${this._options.site}/g/${id}`)
.then(async (res) =>
parseDoujinInfo(
load(res.data),
this.options.site.split('nhentai.')[1] as 'to'
this._options.site.split('nhentai.')[1] as 'to',
this._options.site.includes('net')
? await getAPIGalleryPages(this.#axios, res.data)
: undefined
)
)
.catch((err) => {
Expand All @@ -118,8 +153,8 @@ export class NHentai {
* @param id ID of the doujin to check
*/
public validate = (id: string | number): Promise<boolean> =>
axios
.get(`${this.options.site}/g/${id}`)
this.#axios
.get(`${this._options.site}/g/${id}`)
.then(() => true)
.catch(() => false)
}
12 changes: 5 additions & 7 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
export const sites = ['nhentai.to', 'nhentai.xxx', 'nhentai.website'] as const
export const sites = ['nhentai.to', 'nhentai.net'] as const

export const baseURLS = {
to: 'https://nhentai.to',
xxx: 'https://nhentai.xxx',
website: 'https://nhentai.website'
}
net: 'https://nhentai.net'
} as const

export const imageSites = {
to: 'cdn.dogehls.xyz',
xxx: 'cdn.nhentai.xxx',
website: 'cdn.dogehls.xyz'
}
net: /t[357].nhentai.net/
} as const
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './constants'
export * from './Classes'
export * from './util'
Loading

0 comments on commit c4ae9db

Please sign in to comment.