diff --git a/.env.schema b/.env.schema
index b480e71..444ad64 100644
--- a/.env.schema
+++ b/.env.schema
@@ -11,4 +11,4 @@ REDIS_URL = redis://default:somenicepassword@redis-666.c10.us-east-6-6.ec666.clo
EXPIRE_CACHE = 1
# you must identify your origin, if not set it will use default
-USER_AGENT = "lustpress/1.6.0 Node.js/16.9.1"
\ No newline at end of file
+USER_AGENT = "lustpress/8.0.1 Node.js/22.22.0"
\ No newline at end of file
diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index b51ea71..0000000
--- a/.eslintignore
+++ /dev/null
@@ -1,2 +0,0 @@
-node_modules/
-build/
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index bcf3255..0000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,41 +0,0 @@
-{
- "env": {
- "es2021": true,
- "node": true,
- "commonjs": true
- },
- "extends": [
- "eslint:recommended",
- "plugin:@typescript-eslint/recommended"
- ],
- "parser": "@typescript-eslint/parser",
- "parserOptions": {
- "ecmaVersion": 12,
- "sourceType": "module"
- },
- "plugins": [
- "@typescript-eslint"
- ],
- "rules": {
- "linebreak-style": 0,
- "quotes": [
- "error",
- "double"
- ],
- "semi": [
- "error",
- "always"
- ],
- "no-empty": "error",
- "no-func-assign": "error",
- "no-case-declarations": "off",
- "no-unreachable": "error",
- "no-eval": "error",
- "no-global-assign": "error",
- "@typescript-eslint/no-explicit-any": ["off"],
- "indent": [
- "error",
- 2
- ]
- }
-}
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a0f1689..758651d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,26 +6,28 @@ on:
pull_request:
branches: [ master ]
-jobs:
- build:
+permissions:
+ contents: read
+jobs:
+ test:
runs-on: ubuntu-latest
- strategy:
- matrix:
- node-version: [16.x]
-
steps:
- - uses: actions/checkout@v3
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v3
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
- node-version: ${{ matrix.node-version }}
+ persist-credentials: false
+
+ - name: Setup Node
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
+ with:
+ node-version: 24
+
- name: Install dependencies
run: npm install
- name: Check lint
run: npm run lint
-
+
- name: Build
run: npm run build
\ No newline at end of file
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index fb974e9..4ec9583 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -6,22 +6,35 @@ on:
pull_request:
branches: [ master ]
+permissions:
+ contents: read
+ packages: write
+
jobs:
build:
runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- - name: Log into GitHub Container Registry
- run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
-
- - name: Get the package.json version
- id: package-version
- run: echo ::set-output name=version::$(jq -r .version package.json)
- - name: Build the Docker image
- run: docker build . --file Dockerfile --tag ghcr.io/${{ github.repository }}:${{ steps.package-version.outputs.version }}
- - name: Tag the Docker image
- run: docker tag ghcr.io/${{ github.repository }}:${{ steps.package-version.outputs.version }} ghcr.io/${{ github.repository }}:latest
- - name: Push the Docker image
- run: docker push ghcr.io/${{ github.repository }} --all-tags
-
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
+
+ - name: Set image name
+ run: echo "IMAGE_NAME=ghcr.io/$GITHUB_REPOSITORY" >> $GITHUB_ENV
+
+ - name: Log into GitHub Container Registry
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
+
+ - name: Get the package.json version
+ run: |
+ VERSION=$(jq -r .version package.json)
+ echo "IMAGE_TAG=$VERSION" >> $GITHUB_ENV
+
+ - name: Build the Docker image
+ run: docker build . --file Dockerfile --tag "$IMAGE_NAME:$IMAGE_TAG"
+
+ - name: Tag the Docker image
+ run: docker tag "$IMAGE_NAME:$IMAGE_TAG" "$IMAGE_NAME:latest"
+
+ - name: Push the Docker image
+ run: docker push "$IMAGE_NAME" --all-tags
\ No newline at end of file
diff --git a/.github/workflows/eporner.yml b/.github/workflows/eporner.yml
new file mode 100644
index 0000000..d6e7370
--- /dev/null
+++ b/.github/workflows/eporner.yml
@@ -0,0 +1,36 @@
+name: Eporner test
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
+ with:
+ persist-credentials: false
+
+ - name: Setup Node
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
+ with:
+ node-version: 24
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Install Playwright browsers
+ run: npx playwright install --with-deps chromium
+
+ - name: Build
+ run: npm run build
+
+ - name: Eporner test
+ run: npm run test:eporner
\ No newline at end of file
diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml
index 2892a4e..fb24b69 100644
--- a/.github/workflows/playground.yml
+++ b/.github/workflows/playground.yml
@@ -6,21 +6,32 @@ on:
pull_request:
branches: [ master ]
+permissions:
+ contents: write
+
jobs:
build-and-deploy:
concurrency: ci-${{ github.ref }}
runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v3
- - name: Installing dependencies
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
+ with:
+ persist-credentials: false
+
+ - name: Setup Node
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
+ with:
+ node-version: 24
+
+ - name: Install dependencies
run: npm install
+
- name: Generating docs
run: npm run build:apidoc
- name: Deploy
- uses: JamesIves/github-pages-deploy-action@v4.4.1
+ uses: JamesIves/github-pages-deploy-action@9d877eea73427180ae43cf98e8914934fe157a1a
with:
branch: gh-pages
folder: docs
\ No newline at end of file
diff --git a/.github/workflows/pornhub.yml b/.github/workflows/pornhub.yml
index fc1c86f..c5a10f4 100644
--- a/.github/workflows/pornhub.yml
+++ b/.github/workflows/pornhub.yml
@@ -6,23 +6,29 @@ on:
pull_request:
branches: [ master ]
-jobs:
- build:
+permissions:
+ contents: read
+jobs:
+ test:
runs-on: ubuntu-latest
- strategy:
- matrix:
- node-version: [16.x]
-
steps:
- - uses: actions/checkout@v3
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v3
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
- node-version: ${{ matrix.node-version }}
+ persist-credentials: false
+
+ - name: Setup Node
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
+ with:
+ node-version: 24
+
- name: Install dependencies
run: npm install
+
+ - name: Install Playwright browsers
+ run: npx playwright install --with-deps chromium
+
- name: Build
run: npm run build
diff --git a/.github/workflows/redtube.yml b/.github/workflows/redtube.yml
index 1818296..6d092c4 100644
--- a/.github/workflows/redtube.yml
+++ b/.github/workflows/redtube.yml
@@ -6,23 +6,29 @@ on:
pull_request:
branches: [ master ]
-jobs:
- build:
+permissions:
+ contents: read
+jobs:
+ test:
runs-on: ubuntu-latest
- strategy:
- matrix:
- node-version: [16.x]
-
steps:
- - uses: actions/checkout@v3
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v3
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
- node-version: ${{ matrix.node-version }}
+ persist-credentials: false
+
+ - name: Setup Node
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
+ with:
+ node-version: 24
+
- name: Install dependencies
run: npm install
+
+ - name: Install Playwright browsers
+ run: npx playwright install --with-deps chromium
+
- name: Build
run: npm run build
diff --git a/.github/workflows/status.yml b/.github/workflows/status.yml
index aa4acf7..7451d86 100644
--- a/.github/workflows/status.yml
+++ b/.github/workflows/status.yml
@@ -6,24 +6,28 @@ on:
pull_request:
branches: [ master ]
-jobs:
- build:
+permissions:
+ contents: read
+jobs:
+ test:
runs-on: ubuntu-latest
- strategy:
- matrix:
- node-version: [16.x]
-
steps:
- - uses: actions/checkout@v3
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v3
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
- node-version: ${{ matrix.node-version }}
+ persist-credentials: false
+
+ - name: Setup Node
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
+ with:
+ node-version: 24
+
- name: Install dependencies
run: npm install
+
- name: Build
run: npm run build
+
- name: Check status code
run: npm run test
\ No newline at end of file
diff --git a/.github/workflows/txxx.yml b/.github/workflows/txxx.yml
new file mode 100644
index 0000000..d628a09
--- /dev/null
+++ b/.github/workflows/txxx.yml
@@ -0,0 +1,36 @@
+name: Txxx test
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
+ with:
+ persist-credentials: false
+
+ - name: Setup Node
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
+ with:
+ node-version: 24
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Install Playwright browsers
+ run: npx playwright install --with-deps chromium
+
+ - name: Build
+ run: npm run build
+
+ - name: Txxx test
+ run: npm run test:txxx
\ No newline at end of file
diff --git a/.github/workflows/xhamster.yml b/.github/workflows/xhamster.yml
index ad13452..053c580 100644
--- a/.github/workflows/xhamster.yml
+++ b/.github/workflows/xhamster.yml
@@ -6,23 +6,29 @@ on:
pull_request:
branches: [ master ]
-jobs:
- build:
+permissions:
+ contents: read
+jobs:
+ test:
runs-on: ubuntu-latest
- strategy:
- matrix:
- node-version: [16.x]
-
steps:
- - uses: actions/checkout@v3
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v3
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
- node-version: ${{ matrix.node-version }}
+ persist-credentials: false
+
+ - name: Setup Node
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
+ with:
+ node-version: 24
+
- name: Install dependencies
run: npm install
+
+ - name: Install Playwright browsers
+ run: npx playwright install --with-deps chromium
+
- name: Build
run: npm run build
diff --git a/.github/workflows/xnxx.yml b/.github/workflows/xnxx.yml
index f0fe3a0..55d56a9 100644
--- a/.github/workflows/xnxx.yml
+++ b/.github/workflows/xnxx.yml
@@ -6,23 +6,29 @@ on:
pull_request:
branches: [ master ]
-jobs:
- build:
+permissions:
+ contents: read
+jobs:
+ test:
runs-on: ubuntu-latest
- strategy:
- matrix:
- node-version: [16.x]
-
steps:
- - uses: actions/checkout@v3
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v3
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
- node-version: ${{ matrix.node-version }}
+ persist-credentials: false
+
+ - name: Setup Node
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
+ with:
+ node-version: 24
+
- name: Install dependencies
run: npm install
+
+ - name: Install Playwright browsers
+ run: npx playwright install --with-deps chromium
+
- name: Build
run: npm run build
diff --git a/.github/workflows/xvideos.yml b/.github/workflows/xvideos.yml
index 2e0b9df..5a675b7 100644
--- a/.github/workflows/xvideos.yml
+++ b/.github/workflows/xvideos.yml
@@ -6,23 +6,29 @@ on:
pull_request:
branches: [ master ]
-jobs:
- build:
+permissions:
+ contents: read
+jobs:
+ test:
runs-on: ubuntu-latest
- strategy:
- matrix:
- node-version: [16.x]
-
steps:
- - uses: actions/checkout@v3
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v3
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
- node-version: ${{ matrix.node-version }}
+ persist-credentials: false
+
+ - name: Setup Node
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
+ with:
+ node-version: 24
+
- name: Install dependencies
run: npm install
+
+ - name: Install Playwright browsers
+ run: npx playwright install --with-deps chromium
+
- name: Build
run: npm run build
diff --git a/.github/workflows/youporn.yml b/.github/workflows/youporn.yml
index 6c3c02b..47391fb 100644
--- a/.github/workflows/youporn.yml
+++ b/.github/workflows/youporn.yml
@@ -6,23 +6,29 @@ on:
pull_request:
branches: [ master ]
-jobs:
- build:
+permissions:
+ contents: read
+jobs:
+ test:
runs-on: ubuntu-latest
- strategy:
- matrix:
- node-version: [16.x]
-
steps:
- - uses: actions/checkout@v3
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v3
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
- node-version: ${{ matrix.node-version }}
+ persist-credentials: false
+
+ - name: Setup Node
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
+ with:
+ node-version: 24
+
- name: Install dependencies
run: npm install
+
+ - name: Install Playwright browsers
+ run: npx playwright install --with-deps chromium
+
- name: Build
run: npm run build
diff --git a/CLOSING_REMARKS.md b/CLOSING_REMARKS.md
index 039f171..6705f39 100644
--- a/CLOSING_REMARKS.md
+++ b/CLOSING_REMARKS.md
@@ -1,22 +1,17 @@
# Closing remarks
-I hope you have found this project useful. All the major credit really goes to the all the doujin sites, which
+I hope you have found this project useful. All the major credit really goes to the all the r18 sites, which
allows this services to operate.
-- [PornHub](https://pornhub.com)
-- [Xnxx](https://xnxx.com)
-- [RedTube](https://redtube.com)
-- [Xvideos](https://xvideos.com)
-- [Xhamster](https://xhamster.com)
-- [YouPorn](https://youporn.com)
-- [JavHD](https://javhd.com)
-
Core dependencies:
- [express](https://github.com/expressjs/express)
- [cheerio](https://cheerio.js.org/)
- [keyv](https://github.com/jaredwray/keyv)
-# Alternative-links
-Just in case if https://lust.scathach.id down, here some alternative deployment
+## Frequently asked questions
+**Q: The website response is slow**
+> That's unfortunate, this repository was opensource already, You can host and deploy Lustpress with your own instance. Any fixes and improvements will updating to this repo.
-- https://lustpress.cyclic.app
+> **March 11, 2026**:
+We have discontinued providing public APIs and playground services due to ongoing abuse and excessive usage.
+To continue using Lustpress, please deploy and run your own self-hosted instance.
diff --git a/Dockerfile b/Dockerfile
index bc73214..30e5f18 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:latest
+FROM node:22
WORKDIR /srv/app
COPY package*.json ./
diff --git a/README.md b/README.md
index 4d2395b..cd22095 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,14 @@
-

+

-
RESTful and experimental API for the PornHub and other adult videos
+
RESTful and experimental API for PornHub and other R18 websites
Lustpress stand for Lust and **Express**, rebuild from [Jandapress](https://github.com/sinkaroid/jandapress) with completely different approach.
-The motivation of this project is to bring you an actionable data related to pornhub and other adult videos with gather, similar design pattern, endpoint bindings and consistent structure in mind.
+The motivation of this project is to bring you an actionable data related to pornhub and other r18 sites with gather, similar design pattern, endpoint bindings, and consistent structure in mind.
Playground •
Contributing •
@@ -17,49 +17,57 @@ The motivation of this project is to bring you an actionable data related to por
---
-

+

- [Lustpress](#)
- [The problem](#the-problem)
- [The solution](#the-solution)
- [Features](#features)
- - [Lustpress avaibility](#lustpress-avaibility)
+ - [Running tests](#Running-tests)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Docker](#docker)
- [Manual](#manual)
- - [Running tests](#running-tests)
+ - [Tests case](#tests)
- [Playground](https://sinkaroid.github.io/lustpress)
- [Routing](#playground)
- [Status response](#status-response)
- [CLosing remarks](https://github.com/sinkaroid/lustpress/blob/master/CLOSING_REMARKS.md)
- [Alternative links](https://github.com/sinkaroid/lustpress/blob/master/CLOSING_REMARKS.md#alternative-links)
- [Pronunciation](#Pronunciation)
- - [Client libraries / Wrappers](#client-libraries--wrappers)
+ - [Client libraries](#client-libraries)
- [Acknowledgements](#acknowledgements)
- [Legal](#legal)
+ - [Discontinued playground](#frequently-asked-questions)
+ - [again, discontinued playground](#frequently-asked-questions)
## The problem
-Even official API is exists, they were lack and bad behavior, every single site has own structure and different way to interacts with.
-Instead making lot of abstraction and enumerating them manually, You can rely on lustpress to make less of pain. The current state is FREE to use, all anonymous usage is allowed no authentication and CORS enabled.
+
+Many developers consume r18 websites as a source of data when building web applications. However, most of these sites — such as pornhub, redtube, and others — do not provide official APIs or public resources that can be easily integrated into applications.
+
+As a result, developers often need to implement their own scraping logic, build multiple abstractions, and manually maintain integrations for each site.
+
+Lustpress aims to simplify this process by providing a unified interface for accessing data across multiple r18 sites. Instead of maintaining separate implementations, developers can rely on Lustpress to reduce complexity and development overhead.
+
+The current state of the service is **free to use**, meaning anonymous usage is allowed. No authentication is required, and **CORS is enabled** to support browser-based applications.
## The solution
-Don't make it long, make it short, all processed through single rest
-

+Don't make it long, make it short. All processed through single rest endpoint bindings
+

## Features
-- Ratelimiting and throttling
-- Gather the most adult videos on the internet
-- Objects taken are consistent structure
-- Objects taken is re-appended to make extendable
-- All in one: get, search, and random methods
-- In the future we may implement JWT authentication
-- Pure scraping, does not hit the API (if exists)
+- Aggregates data from multiple sites.
+- Provides a consistent and structured response format across all sources.
+- Extracted objects are normalized and reassembled to support extensibility.
+- Unified interface supporting **get**, **search**, and **random** methods.
+- Planned support for optional **JWT authentication** in future releases.
+- Primarily based on pure scraping techniques (with limited exceptions where required).
+
+## Running tests
+Some tests may fail in CI environments because certain websites restrict or block automated requests originating from CI infrastructure and shared IP ranges, but trying to keep up
-## Lustpress avaibility
-**Features availability** that Lustpress has
| Site | Status | Get | Search | Random | Related |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | ------ | ------ | ------- |
| `pornhub` | [](https://github.com/sinkaroid/lustpress/actions/workflows/pornhub.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
@@ -68,12 +76,12 @@ Don't make it long, make it short, all processed through single rest
| `xvideos` | [](https://github.com/sinkaroid/lustpress/actions/workflows/xvideos.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
| `xhamster` | [](https://github.com/sinkaroid/lustpress/actions/workflows/xhamster.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
| `youporn` | [](https://github.com/sinkaroid/lustpress/actions/workflows/youporn.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
-| `eporner` | — | `Yes` | `Yes` | `Yes` | `Yes` |
-| `txxx` | — | `Yes` | `Yes` | `Yes` | `Yes` |
+| `eporner` | [](https://github.com/sinkaroid/lustpress/actions/workflows/eporner.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
+| `txxx` | [](https://github.com/sinkaroid/lustpress/actions/workflows/txxx.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
## Prerequisites
- | NOTE: NodeJS 16.x or higher |
+ NOTE: NodeJS 22.x or higher |
To handle several requests from each website, You will also need [Redis](https://redis.io/) for persistent caching, free tier is available on [Redis Labs](https://redislabs.com/), You can also choose another adapters as we using [keyv](https://github.com/jaredwray/keyv) Key-value storage with support for multiple backends. When you choosing your own adapter, all data must be stored with `
` type.
@@ -95,7 +103,7 @@ REDIS_URL = redis://default:somenicepassword@redis-666.c10.us-east-6-6.ec666.clo
EXPIRE_CACHE = 1
# you must identify your origin, if not set it will use default
-USER_AGENT = "lustpress/1.6.1 Node.js/16.9.1"
+USER_AGENT = "lustpress/8.0.1 Node.js/22.22.0"
```
### Docker
@@ -110,7 +118,7 @@ docker run -d \
-p 3000:3000 \
-e REDIS_URL='redis://default:somenicepassword@redis-666.c10.us-east-6-6.ec666.cloud.redislabs.com:1337' \
-e EXPIRE_CACHE='1' \
- -e USER_AGENT='lustpress/1.6.1 Node.js/16.9.1' \
+ -e USER_AGENT='lustpress/8.0.1 Node.js/22.22.0' \
ghcr.io/sinkaroid/lustpress:latest
```
@@ -125,8 +133,28 @@ docker run -d \
- Lustpress testing and hot reload
- `npm run start:dev`
-## Running tests
-Lustpress testing
+## Tests
+Run the following commands to execute tests for each supported source:
+
+```bash
+# Check whether all supported sites are available for scraping
+npm run test
+
+# Check whether ph and (maybe the others do..) do Solving challenge in their website
+npm run test:mock
+
+# Run tests for individual sources
+npm run test:pornhub
+npm run test:xnxx
+npm run test:redtube
+npm run test:xvideos
+npm run test:xhamster
+npm run test:youporn
+npm run test:eporner
+npm run test:txxx
+```
+
+
### Start the production server
`npm run start:prod`
@@ -134,10 +162,10 @@ Lustpress testing
### Running development server
`npm run start:dev`
-### Check the whole sites, It's available for scraping or not
-`npm run test`
+### Generating playground like swagger from apidoc definition
+`npm run build:apidoc`
-> To running other tests, you can see object scripts in file `package.json`
+> To running other tests, you can see object scripts in file `package.json` or modify the `lustpress.test.ts` according your needs
## Playground
https://sinkaroid.github.io/lustpress
@@ -156,10 +184,10 @@ The missing piece of pornhub.com - https://sinkaroid.github.io/lustpress/#api-po
- sort parameters on search
- "mr", "mv", "tr", "lg"
- Example
- - https://lust.scathach.id/pornhub/get?id=ph63c4e1dc48fe7
- - https://lust.scathach.id/pornhub/search?key=milf&page=2&sort=mr
- - https://lust.scathach.id/pornhub/related?id=ph63c4e1dc48fe7
- - https://lust.scathach.id/pornhub/random
+ - http://localhost:3000/pornhub/get?id=ph63c4e1dc48fe7
+ - http://localhost:3000/pornhub/search?key=milf&page=2&sort=mr
+ - http://localhost:3000/pornhub/related?id=ph63c4e1dc48fe7
+ - http://localhost:3000/pornhub/random
### Xnxx
The missing piece of xnxx.com - https://sinkaroid.github.io/lustpress/#api-xnxx
@@ -171,10 +199,10 @@ The missing piece of xnxx.com - https://sinkaroid.github.io/lustpress/#api-xnxx
- sort parameters on search
- TBD
- Example
- - https://lust.scathach.id/xnxx/get?id=video-17vah71a/makima_y_denji
- - https://lust.scathach.id/xnxx/search?key=bbc&page=2
- - https://lust.scathach.id/xnxx/related?id=video-17vah71a/makima_y_denji
- - https://lust.scathach.id/xnxx/random
+ - http://localhost:3000/xnxx/get?id=video-17vah71a/makima_y_denji
+ - http://localhost:3000/xnxx/search?key=bbc&page=2
+ - http://localhost:3000/xnxx/related?id=video-17vah71a/makima_y_denji
+ - http://localhost:3000/xnxx/random
### RedTube
The missing piece of redtube.com - https://sinkaroid.github.io/lustpress/#api-redtube
@@ -186,10 +214,10 @@ The missing piece of redtube.com - https://sinkaroid.github.io/lustpress/#api-re
- sort parameters on search
- TBD
- Example
- - https://lust.scathach.id/redtube/get?id=42763661
- - https://lust.scathach.id/redtube/search?key=asian&page=2
- - https://lust.scathach.id/redtube/related?id=42763661
- - https://lust.scathach.id/redtube/random
+ - http://localhost:3000/redtube/get?id=42763661
+ - http://localhost:3000/redtube/search?key=asian&page=2
+ - http://localhost:3000/redtube/related?id=42763661
+ - http://localhost:3000/redtube/random
### Xvideos
The missing piece of xvideos.com - https://sinkaroid.github.io/lustpress/#api-xvideos
@@ -201,10 +229,10 @@ The missing piece of xvideos.com - https://sinkaroid.github.io/lustpress/#api-xv
- sort parameters on search
- TBD
- Example
- - https://lust.scathach.id/xvideos/get?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_
- - https://lust.scathach.id/xvideos/search?key=hentai&page=2
- - https://lust.scathach.id/xvideos/related?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_
- - https://lust.scathach.id/xvideos/random
+ - http://localhost:3000/xvideos/get?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_
+ - http://localhost:3000/xvideos/search?key=hentai&page=2
+ - http://localhost:3000/xvideos/related?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_
+ - http://localhost:3000/xvideos/random
### Xhamster
The missing piece of xhamster.com - https://sinkaroid.github.io/lustpress/#api-xhamster
@@ -213,13 +241,11 @@ The missing piece of xhamster.com - https://sinkaroid.github.io/lustpress/#api-x
- **search**, takes parameters : `key`, `?page`, and TBD
- **related**, takes parameters : `id`
- **random**
- - sort parameters on search
- - TBD
- Example
- - https://lust.scathach.id/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
- - https://lust.scathach.id/xhamster/search?key=arab&page=2
- - https://lust.scathach.id/xhamster/related?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
- - https://lust.scathach.id/xhamster/random
+ - http://localhost:3000/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
+ - http://localhost:3000/xhamster/search?key=arab&page=2
+ - http://localhost:3000/xhamster/related?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
+ - http://localhost:3000/xhamster/random
### YouPorn
The missing piece of youporn.com - https://sinkaroid.github.io/lustpress/#api-youporn
@@ -231,22 +257,38 @@ The missing piece of youporn.com - https://sinkaroid.github.io/lustpress/#api-yo
- sort parameters on search
- TBD
- Example
- - https://lust.scathach.id/youporn/get?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps
- - https://lust.scathach.id/youporn/search?key=teen&page=2
- - https://lust.scathach.id/youporn/related?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps
- - https://lust.scathach.id/youporn/random
+ - http://localhost:3000/youporn/get?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps
+ - http://localhost:3000/youporn/search?key=teen&page=2
+ - http://localhost:3000/youporn/related?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps
+ - http://localhost:3000/youporn/random
-### JavHD
-The missing piece of javhd.com - TBA
-- `/javhd` : javhd api
- - **get**, takes parameters : TBD
- - **search**, takes parameters : TBD
- - **related**, takes parameters : TBD
+### Eporner
+https://sinkaroid.github.io/lustpress/#api-eporner
+- `/eporner` : eporner api
+ - **get**, takes parameters : `id`
+ - **search**, takes parameters : `key`, `?page`
+ - **related**, takes parameters : `id`
- **random**
- sort parameters on search
- TBD
- Example
- - TBD
+ - http://localhost:3000/eporner/get?id=ibvqvezXzcs
+ - http://localhost:3000/eporner/search?key=makima&page=2
+ - http://localhost:3000/eporner/related?id=GPOFlSQLukS
+ - http://localhost:3000/eporner/random
+
+### Txxx
+https://sinkaroid.github.io/lustpress/#api-txxx
+- `/txxx` : txxx api
+ - **get**, takes parameters : `id`
+ - **search**, takes parameters : `key`, `?page`
+ - **related**, takes parameters : `id`
+ - **random**
+ - Example
+ - http://localhost:3000/txxx/get?id=5309183
+ - http://localhost:3000/txxx/search?key=femboy&page=1
+ - http://localhost:3000/txxx/related?id=7794034
+ - http://localhost:3000/txxx/random
## Status response
@@ -258,23 +300,23 @@ The missing piece of javhd.com - TBA
## Frequently asked questions
**Q: The website response is slow**
-> That's unfortunate, This repository was opensource already, You can host and deploy Lustpress with your own instance. Any fixes and improvements will updating to this repo.
+> That's unfortunate, this repository was opensource already, You can host and deploy Lustpress with your own instance. Any fixes and improvements will updating to this repo.
+
+> **March 11, 2026**:
+We have discontinued providing public APIs and playground services due to ongoing abuse and excessive usage.
+To continue using Lustpress, please deploy and run your own self-hosted instance.
-**Q: I dont want to host my own instance**
-> That's unfortunate, Hit the "Sponsor this project" button, any kind of donations will helps me to funding the development.
## Pronunciation
`en_US` • **/lʌstˈprɛs/** — "lust" stand for this project and "press" for express.
-## Client libraries / Wrappers
+## Client libraries
Seamlessly integrate with the languages you love, simplified the usage, and intelisense definitions on your IDEs
- TBD
- Or [create your own](https://github.com/sinkaroid/lustpress/edit/master/README.md)
-## Acknowledgements
-I thank you to all the [Scathach's supporter](https://scathachbot.xyz/about) who made this project exists :gajah_ngecrot:
## Legal
This tool can be freely copied, modified, altered, distributed without any attribution whatsoever. However, if you feel
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..c3394fa
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,45 @@
+const tsParser = require("@typescript-eslint/parser");
+const tsPlugin = require("@typescript-eslint/eslint-plugin");
+
+module.exports = [
+ {
+ ignores: ["node_modules/**", "build/**"],
+ },
+ {
+ files: ["**/*.ts"],
+ languageOptions: {
+ parser: tsParser,
+ parserOptions: {
+ ecmaVersion: 12,
+ sourceType: "module",
+ },
+ globals: {
+ module: "readonly",
+ require: "readonly",
+ __dirname: "readonly",
+ __filename: "readonly",
+ process: "readonly",
+ Buffer: "readonly",
+ console: "readonly",
+ },
+ },
+ plugins: {
+ "@typescript-eslint": tsPlugin,
+ },
+ rules: {
+ "no-unused-vars": "error",
+ "no-undef": "error",
+ "linebreak-style": 0,
+ "quotes": ["error", "double"],
+ "semi": ["error", "always"],
+ "no-empty": "error",
+ "no-func-assign": "error",
+ "no-case-declarations": "off",
+ "no-unreachable": "error",
+ "no-eval": "error",
+ "no-global-assign": "error",
+ "@typescript-eslint/no-explicit-any": "off",
+ "indent": ["error", 2],
+ },
+ },
+];
\ No newline at end of file
diff --git a/package.json b/package.json
index b62630b..2522ee5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "lustpress",
- "version": "1.6.3-alpha",
- "description": "RESTful and experimental API for PornHub and other porn sites, which official is lack even isn't exist.",
+ "version": "8.1.2-alpha",
+ "description": "RESTful and experimental API for PH and other R18 websites",
"main": "build/src/index.js",
"scripts": {
"build": "rimraf build && tsc",
@@ -13,20 +13,21 @@
"start:flyctl": "flyctl deploy",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --fix",
- "build:freshdoc": "rimraf docs && rimraf template && rimraf theme.zip",
- "build:template": " npm run build:freshdoc && curl https://codeload.github.com/ScathachGrip/apidocjs-theme/zip/refs/tags/v11 -o theme.zip && unzip theme.zip && mv apidocjs-theme-11 template",
- "build:apidoc": "npm run build:template && apidoc -i src -o ./docs -t ./template/template-scarlet",
- "test:pornhub": "start-server-and-test 3000 \"curl -v http://localhost:3000/pornhub/random | jq '.'\"",
- "test:xnxx": "start-server-and-test 3000 \"curl -v http://localhost:3000/xnxx/random | jq '.'\"",
- "test:redtube": "start-server-and-test 3000 \"curl -v http://localhost:3000/redtube/random | jq '.'\"",
- "test:xvideos": "start-server-and-test 3000 \"curl -v http://localhost:3000/xvideos/random | jq '.'\"",
- "test:xhamster": "start-server-and-test 3000 \"curl -v http://localhost:3000/xhamster/random | jq '.'\"",
- "test:youporn": "start-server-and-test 3000 \"curl -v http://localhost:3000/youporn/random | jq '.'\""
+ "build:freshdoc": "rimraf docs",
+ "build:apidoc": "npm run build:freshdoc && npx apidoc -i src -o docs",
+ "test:pornhub": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^pornhub$ test/lustpress.test.ts\"",
+ "test:xnxx": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^xnxx$ test/lustpress.test.ts\"",
+ "test:redtube": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^redtube$ test/lustpress.test.ts\"",
+ "test:xvideos": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^xvideos$ test/lustpress.test.ts\"",
+ "test:xhamster": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^xhamster$ test/lustpress.test.ts\"",
+ "test:youporn": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^youporn$ test/lustpress.test.ts\"",
+ "test:eporner": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^eporner$ test/lustpress.test.ts\"",
+ "test:txxx": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^txxx$ test/lustpress.test.ts\""
},
"apidoc": {
"title": "Lustpress API Documentation",
- "url": "https://lust.scathach.id",
- "sampleUrl": "https://lust.scathach.id",
+ "url": "http://localhost:3000",
+ "sampleUrl": "http://localhost:3000",
"name": "Lustpress"
},
"keywords": [],
@@ -37,34 +38,34 @@
},
"license": "MIT",
"dependencies": {
- "@keyv/redis": "^2.5.7",
- "cheerio": "^1.0.0-rc.12",
- "cors": "^2.8.5",
- "dotenv": "^16.0.3",
- "express": "^4.18.2",
- "express-rate-limit": "^6.7.0",
- "express-slow-down": "^1.6.0",
- "keyv": "^4.5.2",
- "phin": "^3.7.0",
- "pino": "^8.11.0",
- "pino-pretty": "^10.0.0"
+ "@keyv/redis": "^5.1.6",
+ "cheerio": "^1.2.0",
+ "cors": "^2.8.6",
+ "dotenv": "^17.3.1",
+ "express": "^5.2.1",
+ "express-rate-limit": "^8.3.1",
+ "express-slow-down": "^3.1.0",
+ "keyv": "^5.6.0",
+ "phin": "^3.7.1",
+ "pino": "^10.3.1",
+ "pino-pretty": "^13.1.3",
+ "playwright": "^1.58.2"
},
"devDependencies": {
- "@types/cors": "^2.8.13",
- "@types/express": "^4.17.17",
- "@types/express-slow-down": "^1.3.2",
- "@types/node": "^18.15.11",
- "@typescript-eslint/eslint-plugin": "^5.58.0",
- "@typescript-eslint/parser": "^5.58.0",
- "apidoc": "0.29.0",
- "eslint": "^8.38.0",
- "rimraf": "^5.0.0",
- "start-server-and-test": "^2.0.0",
- "ts-node": "^10.9.1",
+ "@types/cors": "^2.8.19",
+ "@types/express": "^5.0.6",
+ "@types/node": "^25.4.0",
+ "@typescript-eslint/eslint-plugin": "^8.57.0",
+ "@typescript-eslint/parser": "^8.57.0",
+ "apidoc": "^1.2.0",
+ "eslint": "^10.0.3",
+ "rimraf": "^6.1.3",
+ "start-server-and-test": "^2.1.5",
+ "ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
- "typescript": "5.0.4"
+ "typescript": "5.9.3"
},
"engines": {
- "node": ">=14"
+ "node": ">=22"
}
-}
+}
\ No newline at end of file
diff --git a/resources/project/images/bnnuy.png b/resources/project/images/bnnuy.png
new file mode 100644
index 0000000..96a0184
Binary files /dev/null and b/resources/project/images/bnnuy.png differ
diff --git a/resources/project/images/coverage.png b/resources/project/images/coverage.png
new file mode 100644
index 0000000..3183dbc
Binary files /dev/null and b/resources/project/images/coverage.png differ
diff --git a/resources/project/images/lustpress-node_1.png b/resources/project/images/lustpress-node_1.png
new file mode 100644
index 0000000..a47ee2d
Binary files /dev/null and b/resources/project/images/lustpress-node_1.png differ
diff --git a/src/LustPress.ts b/src/LustPress.ts
index be4e9d9..857c2b9 100644
--- a/src/LustPress.ts
+++ b/src/LustPress.ts
@@ -1,54 +1,159 @@
+import { URL } from "node:url";
+import { chromium } from "playwright";
import p, { IResponse } from "phin";
import Keyv from "keyv";
+import KeyvRedis from "@keyv/redis";
import pkg from "../package.json";
-
-const keyv = new Keyv(process.env.REDIS_URL);
+const keyv = process.env.REDIS_URL
+ ? new Keyv({ store: new KeyvRedis(process.env.REDIS_URL) })
+ : new Keyv();
keyv.on("error", err => console.log("Connection Error", err));
const ttl = 1000 * 60 * 60 * Number(process.env.EXPIRE_CACHE);
-
class LustPress {
+
url: string;
useragent: string;
+ private cookieCache: { [domain: string]: string } = {};
+
+
constructor() {
this.url = "";
- this.useragent = `${pkg.name}/${pkg.version} Node.js/16.9.1`;
+ this.useragent = `${pkg.name}/${pkg.version} Node.js/${process.versions.node}`;
+ }
+
+ async getCookies(url: string): Promise {
+ const browser = await chromium.launch({ headless: true });
+
+ const context = await browser.newContext({
+ userAgent: process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/${process.versions.node}`
+ });
+
+ const page = await context.newPage();
+ await page.goto(url, { waitUntil: "domcontentloaded" });
+ await page.waitForLoadState("networkidle");
+ const cookies = await context.cookies();
+ await browser.close();
+ return cookies.map(c => `${c.name}=${c.value}`).join("; ");
}
/**
- * Fetch body from url and check if it's cached
- * @param url url to fetch
- * @returns Buffer
- */
+ * Fetch body from url and check if it's cached
+ * @param url url to fetch
+ * @returns Buffer
+ */
async fetchBody(url: string): Promise {
+
const cached = await keyv.get(url);
if (cached) {
console.log("Fetching from cache");
return cached;
+
} else if (url.includes("/random")) {
+
console.log("Random should not be cached");
- const res = await p({
+
+ const isPornhub = /pornhub\.com/i.test(url);
+
+ let cookieHeader = "";
+
+ if (isPornhub) {
+ const domain = new URL(url).hostname;
+
+ if (this.cookieCache[domain]) {
+ console.log("Using cached cookie");
+ cookieHeader = this.cookieCache[domain];
+ } else {
+ console.log("Solving challenge via playwright");
+ cookieHeader = await this.getCookies(url);
+ this.cookieCache[domain] = cookieHeader;
+ }
+ }
+
+ console.log(`Fetching from ${isPornhub ? "phin (cookie)" : "phin"}`);
+
+ const res = await p({
url: url,
- "headers": {
- "User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/16.9.1`,
- },
- followRedirects: true
- });
- return res.body;
- } else {
- console.log("Fetching from source");
- url = url.replace(/\/\//g, "/");
- const res = await p({
- url: url,
- "headers": {
- "User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/16.9.1`,
+ headers: {
+ "User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/${process.versions.node}`,
+ ...(cookieHeader && { "Cookie": cookieHeader })
},
followRedirects: true
});
+
+ if (isPornhub && res.statusCode !== 200) {
+ const domain = new URL(url).hostname;
+ console.log("Cookie invalid, clearing cache and retrying via playwright");
+ delete this.cookieCache[domain];
+ const newCookie = await this.getCookies(url);
+ this.cookieCache[domain] = newCookie;
+ const retry = await p({
+ url: url,
+ headers: {
+ "User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/${process.versions.node}`,
+ "Cookie": newCookie
+ },
+ followRedirects: true
+ });
+ return retry.body;
+ }
+
+ return res.body;
+
+ } else {
+
+ console.log("Fetching from source");
+ url = url.replace(/\/\//g, "/");
+ const isPornhub = /pornhub\.com/i.test(url);
+
+ let cookieHeader = "";
+
+ if (isPornhub) {
+ const domain = new URL(url).hostname;
+
+ if (this.cookieCache[domain]) {
+ console.log("Using cached cookie");
+ cookieHeader = this.cookieCache[domain];
+ } else {
+ console.log("Solving challenge via playwright");
+ cookieHeader = await this.getCookies(url);
+ this.cookieCache[domain] = cookieHeader;
+ }
+ }
+
+ console.log(`Fetching from ${isPornhub ? "phin (cookie)" : "phin"}`);
+
+ const res = await p({
+ url: url,
+ headers: {
+ "User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/16.9.1`,
+ ...(cookieHeader && { "Cookie": cookieHeader })
+ },
+ followRedirects: true
+ });
+
+ if (isPornhub && res.statusCode !== 200) {
+ const domain = new URL(url).hostname;
+ console.log("Cookie invalid, clearing cache and retrying via playwright");
+ delete this.cookieCache[domain];
+ const newCookie = await this.getCookies(url);
+ this.cookieCache[domain] = newCookie;
+ const retry = await p({
+ url: url,
+ headers: {
+ "User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/16.9.1`,
+ "Cookie": newCookie
+ },
+ followRedirects: true
+ });
+ return retry.body;
+ }
+
await keyv.set(url, res.body, ttl);
+
return res.body;
}
}
@@ -110,7 +215,7 @@ class LustPress {
}
}
}
-
+
/**
* convert seconds to minute
* @param seconds seconds to convert
@@ -143,9 +248,9 @@ class LustPress {
* @returns >
*/
async getServer(): Promise {
- const raw = await p({
- "url": "http://ip-api.com/json",
- "parse": "json"
+ const raw = await p({
+ "url": "http://ip-api.com/json",
+ "parse": "json"
}) as IResponse;
const data = raw.body as unknown as { country: string, regionName: string };
return `${data.country}, ${data.regionName}`;
diff --git a/src/controller/eporner/epornerRandom.ts b/src/controller/eporner/epornerRandom.ts
index 4589168..77bb927 100644
--- a/src/controller/eporner/epornerRandom.ts
+++ b/src/controller/eporner/epornerRandom.ts
@@ -4,9 +4,6 @@ import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
-import LustPress from "../../LustPress";
-
-const lust = new LustPress();
export async function randomEporner(req: Request, res: Response) {
/**
diff --git a/src/controller/eporner/epornerSearch.ts b/src/controller/eporner/epornerSearch.ts
index 1daa093..131a3b2 100644
--- a/src/controller/eporner/epornerSearch.ts
+++ b/src/controller/eporner/epornerSearch.ts
@@ -1,7 +1,7 @@
import { scrapeContent } from "../../scraper/eporner/epornerSearchController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
-import { maybeError, spacer } from "../../utils/modifier";
+import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
export async function searchEporner(req: Request, res: Response) {
@@ -45,28 +45,28 @@ export async function searchEporner(req: Request, res: Response) {
if (isNaN(page)) throw Error("Parameter page must be a number");
const slug = key
- .toLowerCase()
- .trim()
- .replace(/\s+/g, "-");
+ .toLowerCase()
+ .trim()
+ .replace(/\s+/g, "-");
const url =
page === 1
- ? `${c.EPORNER}/tag/${slug}/`
- : `${c.EPORNER}/tag/${slug}/${page}/`;
+ ? `${c.EPORNER}/tag/${slug}/`
+ : `${c.EPORNER}/tag/${slug}/${page}/`;
const data = await scrapeContent(url);
logger.info({
- path: req.path,
- query: req.query,
- method: req.method,
- ip: req.ip,
- useragent: req.get("User-Agent")
+ path: req.path,
+ query: req.query,
+ method: req.method,
+ ip: req.ip,
+ useragent: req.get("User-Agent")
});
return res.json(data);
- } catch (err) {
+ } catch (err) {
const e = err as Error;
res.status(400).json(maybeError(false, e.message));
- }
+ }
}
\ No newline at end of file
diff --git a/src/controller/txxx/txxxGetRelated.ts b/src/controller/txxx/txxxGetRelated.ts
index c21e6ba..a5fc00e 100644
--- a/src/controller/txxx/txxxGetRelated.ts
+++ b/src/controller/txxx/txxxGetRelated.ts
@@ -14,7 +14,7 @@ function getRelatedApiUrl(videoId: string, page = 1, count = 50): string {
const thousand = Math.floor(id / 1_000) * 1_000;
return (
- `https://txxx.com/api/json/videos_related2/` +
+ "https://txxx.com/api/json/videos_related2/" +
`432000/${count}/${million}/${thousand}/${id}.all.${page}.json`
);
}
diff --git a/src/controller/txxx/txxxRandom.ts b/src/controller/txxx/txxxRandom.ts
index be198c4..6c339b9 100644
--- a/src/controller/txxx/txxxRandom.ts
+++ b/src/controller/txxx/txxxRandom.ts
@@ -6,53 +6,53 @@ import { logger } from "../../utils/logger";
const lust = new LustPress();
export async function randomTxxx(req: Request, res: Response) {
- try {
- const apiUrl =
+ try {
+ const apiUrl =
"https://txxx.com/api/json/videos2/14400/str/most-popular/60/..1.all..day.json";
- const buffer = await lust.fetchBody(apiUrl);
- const rawData = JSON.parse(buffer.toString("utf-8"));
+ const buffer = await lust.fetchBody(apiUrl);
+ const rawData = JSON.parse(buffer.toString("utf-8"));
- const videos = Array.isArray(rawData.videos)
- ? rawData.videos
- : [];
+ const videos = Array.isArray(rawData.videos)
+ ? rawData.videos
+ : [];
- if (videos.length === 0) {
- throw new Error("No videos returned from upstream");
- }
-
- // Pick one random video
- const v = videos[Math.floor(Math.random() * videos.length)];
-
- const data = {
- video_id: v.video_id,
- title: v.title,
- dir: v.dir,
- duration: v.duration,
- views: v.video_viewed,
- rating: v.rating,
- uploaded: v.post_date,
- likes: v.likes,
- dislikes: v.dislikes,
- image: v.scr,
- categories: v.categories ? v.categories.split(",") : [],
- embed: `https://txxx.com/embed/${v.video_id}/`,
- };
-
- logger.info({
- path: req.path,
- method: req.method,
- ip: req.ip,
- useragent: req.get("User-Agent"),
- });
-
- return res.json({
- success: true,
- data,
- source: apiUrl,
- });
- } catch (err) {
- const e = err as Error;
- return res.status(400).json(maybeError(false, e.message));
+ if (videos.length === 0) {
+ throw new Error("No videos returned from upstream");
}
+
+ // Pick one random video
+ const v = videos[Math.floor(Math.random() * videos.length)];
+
+ const data = {
+ video_id: v.video_id,
+ title: v.title,
+ dir: v.dir,
+ duration: v.duration,
+ views: v.video_viewed,
+ rating: v.rating,
+ uploaded: v.post_date,
+ likes: v.likes,
+ dislikes: v.dislikes,
+ image: v.scr,
+ categories: v.categories ? v.categories.split(",") : [],
+ embed: `https://txxx.com/embed/${v.video_id}/`,
+ };
+
+ logger.info({
+ path: req.path,
+ method: req.method,
+ ip: req.ip,
+ useragent: req.get("User-Agent"),
+ });
+
+ return res.json({
+ success: true,
+ data,
+ source: apiUrl,
+ });
+ } catch (err) {
+ const e = err as Error;
+ return res.status(400).json(maybeError(false, e.message));
+ }
}
diff --git a/src/controller/txxx/txxxSearch.ts b/src/controller/txxx/txxxSearch.ts
index 4a43986..6b4e8a9 100644
--- a/src/controller/txxx/txxxSearch.ts
+++ b/src/controller/txxx/txxxSearch.ts
@@ -5,59 +5,59 @@ import { maybeError } from "../../utils/modifier";
const lust = new LustPress();
export async function searchTxxx(req: Request, res: Response) {
- try {
- const key = String(req.query.key || "").trim();
- const page = Number(req.query.page || 1);
+ try {
+ const key = String(req.query.key || "").trim();
+ const page = Number(req.query.page || 1);
- if (!key) {
- return res.json({
- success: false,
- error: "Parameter key is required",
- });
- }
+ if (!key) {
+ return res.json({
+ success: false,
+ error: "Parameter key is required",
+ });
+ }
- if (Number.isNaN(page)) {
- return res.json({
- success: false,
- error: "Parameter page must be a number",
- });
- }
+ if (Number.isNaN(page)) {
+ return res.json({
+ success: false,
+ error: "Parameter page must be a number",
+ });
+ }
- const apiUrl =
- `https://txxx.com/api/videos2.php` +
+ const apiUrl =
+ "https://txxx.com/api/videos2.php" +
`?params=259200/str/relevance/60/search..${page}.all..` +
`&s=${encodeURIComponent(key)}`;
- // Fetch from API directly
- const buffer = await lust.fetchBody(apiUrl);
- const rawData = JSON.parse(buffer.toString("utf-8"));
+ // Fetch from API directly
+ const buffer = await lust.fetchBody(apiUrl);
+ const rawData = JSON.parse(buffer.toString("utf-8"));
- const videos = Array.isArray(rawData.videos) ? rawData.videos : [];
+ const videos = Array.isArray(rawData.videos) ? rawData.videos : [];
- const data = videos.map((v: any) => ({
- video_id: v.video_id,
- title: v.title,
- dir: v.dir,
- duration: v.duration,
- views: v.video_viewed,
- rating: v.rating,
- uploaded: v.post_date,
- likes: v.likes,
- dislikes: v.dislikes,
- image: v.scr,
- categories: v.categories ? v.categories.split(",") : [],
- embed: `https://txxx.com/embed/${v.video_id}/`,
- }));
+ const data = videos.map((v: any) => ({
+ video_id: v.video_id,
+ title: v.title,
+ dir: v.dir,
+ duration: v.duration,
+ views: v.video_viewed,
+ rating: v.rating,
+ uploaded: v.post_date,
+ likes: v.likes,
+ dislikes: v.dislikes,
+ image: v.scr,
+ categories: v.categories ? v.categories.split(",") : [],
+ embed: `https://txxx.com/embed/${v.video_id}/`,
+ }));
- return res.json({
- success: true,
- total_count: String(rawData.total_count ?? videos.length),
- pages: Number(rawData.pages ?? 1),
- page,
- data,
- });
- } catch (err) {
- const e = err as Error;
- return res.json(maybeError(false, e.message));
- }
+ return res.json({
+ success: true,
+ total_count: String(rawData.total_count ?? videos.length),
+ pages: Number(rawData.pages ?? 1),
+ page,
+ data,
+ });
+ } catch (err) {
+ const e = err as Error;
+ return res.json(maybeError(false, e.message));
+ }
}
diff --git a/src/scraper/eporner/epornerGetController.ts b/src/scraper/eporner/epornerGetController.ts
index 101f1b1..47f3bd1 100644
--- a/src/scraper/eporner/epornerGetController.ts
+++ b/src/scraper/eporner/epornerGetController.ts
@@ -49,8 +49,9 @@ export async function scrapeContent(url: string) {
this.upVote = $(".likeup i").first().text().trim() || "0";
this.downVote = $(".likedown i").first().text().trim() || "0";
- const jsonLdText = $('script[type="application/ld+json"]').first().html() || "{}";
+ const jsonLdText = $("script[type=\"application/ld+json\"]").first().html() || "{}";
const jsonLd = JSON.parse(jsonLdText);
+ // eslint-disable-next-line no-unused-vars
const isoDuration = jsonLd.duration; // "PT00H8M12S"
this.publish = jsonLd.uploadDate || "None";
@@ -62,10 +63,10 @@ export async function scrapeContent(url: string) {
this.video = `https://www.eporner.com/embed/${code}/`;
this.tags = $("li.vit-category a")
- .map((_, el) => $(el).text().trim()).get();
+ .map((_, el) => $(el).text().trim()).get();
this.models = $("li.vit-pornstar a")
- .map((_, el) => $(el).text().trim()).get();
+ .map((_, el) => $(el).text().trim()).get();
}
}
diff --git a/src/scraper/eporner/epornerSearchController.ts b/src/scraper/eporner/epornerSearchController.ts
index 0855ee7..dd09fcf 100644
--- a/src/scraper/eporner/epornerSearchController.ts
+++ b/src/scraper/eporner/epornerSearchController.ts
@@ -5,66 +5,66 @@ import { ISearchItem } from "../../interfaces";
const lust = new LustPress();
export async function scrapeContent(url: string) {
- try {
- const res = await lust.fetchBody(url);
- const $ = load(res);
+ try {
+ const res = await lust.fetchBody(url);
+ const $ = load(res);
- class EPornerSearch {
- data: ISearchItem[];
+ class EPornerSearch {
+ data: ISearchItem[];
- constructor() {
- this.data = $("#vidresults .mb")
- .map((i, el) => {
- const link =
+ constructor() {
+ this.data = $("#vidresults .mb")
+ .map((i, el) => {
+ const link =
$(el).find(".mbimg a").first().attr("href") || "";
- const match = link.match(/\/(video-|hd-porn\/)([^/]+)/);
+ const match = link.match(/\/(video-|hd-porn\/)([^/]+)/);
- let id = "";
+ let id = "";
- if (match) {
- if (match[1] === "video-") {
- id = `video-${match[2]}`;
- } else {
- id = match[2];
- }
- }
-
- const code = id.startsWith("video-")
- ? id.replace("video-", "")
- : id;
-
- return {
- link: `https://www.eporner.com${link}`,
- id,
- title: $(el).find(".mbtit a").text().trim(),
- image: $(el).find(".mbimg img").attr("src"),
- duration: $(el).find(".mbtim").text().trim(),
- rating: $(el).find(".mbrate").text().trim(),
- views: $(el).find(".mbvie").text().trim(),
- uploader: $(el)
- .find(".mb-uploader a")
- .text()
- .trim(),
- video: code
- ? `https://www.eporner.com/embed/${code}/`
- : "None",
- };
- })
- .get();
+ if (match) {
+ if (match[1] === "video-") {
+ id = `video-${match[2]}`;
+ } else {
+ id = match[2];
+ }
}
- }
- const ep = new EPornerSearch();
- if (ep.data.length === 0) throw Error("No result found");
+ const code = id.startsWith("video-")
+ ? id.replace("video-", "")
+ : id;
- return {
- success: true,
- data: ep.data as ISearchItem[],
- source: url,
- };
- } catch (err) {
- const e = err as Error;
- throw Error(e.message);
+ return {
+ link: `https://www.eporner.com${link}`,
+ id,
+ title: $(el).find(".mbtit a").text().trim(),
+ image: $(el).find(".mbimg img").attr("src"),
+ duration: $(el).find(".mbtim").text().trim(),
+ rating: $(el).find(".mbrate").text().trim(),
+ views: $(el).find(".mbvie").text().trim(),
+ uploader: $(el)
+ .find(".mb-uploader a")
+ .text()
+ .trim(),
+ video: code
+ ? `https://www.eporner.com/embed/${code}/`
+ : "None",
+ };
+ })
+ .get();
+ }
}
+
+ const ep = new EPornerSearch();
+ if (ep.data.length === 0) throw Error("No result found");
+
+ return {
+ success: true,
+ data: ep.data as ISearchItem[],
+ source: url,
+ };
+ } catch (err) {
+ const e = err as Error;
+ throw Error(e.message);
+ }
}
\ No newline at end of file
diff --git a/src/scraper/xhamster/xhamsterGetController.ts b/src/scraper/xhamster/xhamsterGetController.ts
index 18ccba8..2680602 100644
--- a/src/scraper/xhamster/xhamsterGetController.ts
+++ b/src/scraper/xhamster/xhamsterGetController.ts
@@ -12,8 +12,8 @@ export async function scrapeContent(url: string) {
const raw = $("#initials-script").html();
const initials = raw
? JSON.parse(
- raw.replace(/^window\.initials\s*=\s*/, "").replace(/;$/, ""),
- )
+ raw.replace(/^window\.initials\s*=\s*/, "").replace(/;$/, ""),
+ )
: null;
class Xhamster {
@@ -47,7 +47,7 @@ export async function scrapeContent(url: string) {
.filter(Boolean);
const videoScript = scripts.find(
- (s) => s.includes('"videoModel"') && s.includes('"duration"'),
+ (s) => s.includes("\"videoModel\"") && s.includes("\"duration\""),
);
if (videoScript) {
diff --git a/src/scraper/youporn/youpornSearchController.ts b/src/scraper/youporn/youpornSearchController.ts
index b6f8d24..8286987 100644
--- a/src/scraper/youporn/youpornSearchController.ts
+++ b/src/scraper/youporn/youpornSearchController.ts
@@ -64,7 +64,7 @@ export async function scrapeContent(url: string) {
.get();
this.search = cards
- .map((i, el) => {
+ .map((i) => {
return {
link: `${c.YOUPORN}${this.links[i]}`,
id: this.ids[i],
diff --git a/src/utils/limit-options.ts b/src/utils/limit-options.ts
index c9ddf55..5197e5c 100644
--- a/src/utils/limit-options.ts
+++ b/src/utils/limit-options.ts
@@ -10,7 +10,7 @@ const limiter = rateLimit({
const slow = slowDown({
delayAfter: 50,
windowMs: 15 * 60 * 1000,
- delayMs: 1000,
+ delayMs: () => 1000,
maxDelayMs: 20000,
});
diff --git a/test/lustpress.test.ts b/test/lustpress.test.ts
new file mode 100644
index 0000000..6d434b8
--- /dev/null
+++ b/test/lustpress.test.ts
@@ -0,0 +1,62 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import p from "phin";
+
+const port = process.env.PORT ?? 3000;
+
+type ApiResponse = {
+ success: boolean;
+ data?: {
+ id?: string | number;
+ };
+};
+
+async function run(path: string) {
+ const res = await p({
+ url: `http://localhost:${port}${path}`,
+ parse: "json",
+ timeout: 20000
+ });
+
+ assert.equal(res.statusCode, 200);
+
+ const json = res.body as ApiResponse;
+
+ console.log(JSON.stringify(json, null, 2));
+
+ if (!json.success) {
+ throw new Error("scraper failed");
+ }
+}
+
+test("pornhub", async () => {
+ await run("/pornhub/random");
+});
+
+test("xnxx", async () => {
+ await run("/xnxx/random");
+});
+
+test("redtube", async () => {
+ await run("/redtube/random");
+});
+
+test("xvideos", async () => {
+ await run("/xvideos/random");
+});
+
+test("xhamster", async () => {
+ await run("/xhamster/random");
+});
+
+test("youporn", async () => {
+ await run("/youporn/random");
+});
+
+test("eporner", async () => {
+ await run("/eporner/random");
+});
+
+test("txxx", async () => {
+ await run("/txxx/random");
+});
\ No newline at end of file
diff --git a/test/mock.ts b/test/mock.ts
index 78a6e1b..2f46047 100644
--- a/test/mock.ts
+++ b/test/mock.ts
@@ -1,20 +1,50 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+
import p from "phin";
+import { chromium } from "playwright";
import { load } from "cheerio";
+import pkg from "../package.json";
-const url = "https://www.pornhub.com/view_video.php?viewkey=ph63c4e1dc48fe7";
+const url = "https://www.pornhub.com/view_video.php?viewkey=697a92abd524b";
-async function test() {
- const res = await p({
- url: url,
- "headers": {
- "User-Agent": process.env.USER_AGENT || "lustpress/1.6.0 Node.js/16.9.1",
- },
- });
-
- const $ = load(res.body);
- const title = $("meta[property='og:title']").attr("content");
- console.log(title);
- console.log(res.statusCode);
+async function getCookies() {
+ const browser = await chromium.launch({ headless: true });
+ const context = await browser.newContext();
+
+ const page = await context.newPage();
+ await page.goto(url, { waitUntil: "domcontentloaded" });
+ await page.waitForLoadState("networkidle");
+
+ const cookies = await context.cookies();
+ await browser.close();
+
+ return cookies.map(c => `${c.name}=${c.value}`).join("; ");
}
-test().catch(console.error);
+test("pornhub og:title extraction", async () => {
+
+ const cookieHeader = await getCookies();
+
+ const res = await p({
+ url,
+ headers: {
+ "User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/${process.versions.node}`,
+ "Cookie": cookieHeader
+ },
+ followRedirects: true
+ });
+
+ console.log("Cookie Header:", cookieHeader);
+ console.log("Status:", res.statusCode);
+
+ const html = res.body.toString();
+ // console.log(html.slice(0, 1000));
+
+ const $ = load(html);
+ const title = $("meta[property='og:title']").attr("content");
+
+ console.log("Title:", title);
+
+ assert.ok(title, "og:title should exist");
+});
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index e659f24..2ab7644 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -18,7 +18,8 @@
"strict": true,
"strictBindCallApply": true,
"strictPropertyInitialization": false,
- "declaration": true
+ "declaration": true,
+ "skipLibCheck": true
},
"include": [
"src/**/*"