feat: use Playwright challenge and reuse cookies scraper (#17)

* adjust eslint

* playground depreciation warning

* adjust some tests

* playwright simulate run instead just raw phin

* adjusted ci

* vibe debugging

* update all deps, small ci changes, and strict dockerized build
This commit is contained in:
Indrawan I.
2026-03-13 11:05:40 +07:00
committed by GitHub
parent 09b49f1649
commit e63195035d
37 changed files with 847 additions and 473 deletions

View File

@@ -11,4 +11,4 @@ REDIS_URL = redis://default:somenicepassword@redis-666.c10.us-east-6-6.ec666.clo
EXPIRE_CACHE = 1 EXPIRE_CACHE = 1
# you must identify your origin, if not set it will use default # you must identify your origin, if not set it will use default
USER_AGENT = "lustpress/1.6.0 Node.js/16.9.1" USER_AGENT = "lustpress/8.0.1 Node.js/22.22.0"

View File

@@ -1,2 +0,0 @@
node_modules/
build/

View File

@@ -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
]
}
}

View File

@@ -6,26 +6,28 @@ on:
pull_request: pull_request:
branches: [ master ] branches: [ master ]
jobs: permissions:
build: contents: read
jobs:
test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version: 24
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Check lint - name: Check lint
run: npm run lint run: npm run lint
- name: Build - name: Build
run: npm run build run: npm run build

View File

@@ -6,22 +6,35 @@ on:
pull_request: pull_request:
branches: [ master ] branches: [ master ]
permissions:
contents: read
packages: write
jobs: jobs:
build: build:
runs-on: ubuntu-latest 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

36
.github/workflows/eporner.yml vendored Normal file
View File

@@ -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

View File

@@ -6,21 +6,32 @@ on:
pull_request: pull_request:
branches: [ master ] branches: [ master ]
permissions:
contents: write
jobs: jobs:
build-and-deploy: build-and-deploy:
concurrency: ci-${{ github.ref }} concurrency: ci-${{ github.ref }}
runs-on: ubuntu-latest 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 run: npm install
- name: Generating docs - name: Generating docs
run: npm run build:apidoc run: npm run build:apidoc
- name: Deploy - name: Deploy
uses: JamesIves/github-pages-deploy-action@v4.4.1 uses: JamesIves/github-pages-deploy-action@9d877eea73427180ae43cf98e8914934fe157a1a
with: with:
branch: gh-pages branch: gh-pages
folder: docs folder: docs

View File

@@ -6,23 +6,29 @@ on:
pull_request: pull_request:
branches: [ master ] branches: [ master ]
jobs: permissions:
build: contents: read
jobs:
test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version: 24
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Build - name: Build
run: npm run build run: npm run build

View File

@@ -6,23 +6,29 @@ on:
pull_request: pull_request:
branches: [ master ] branches: [ master ]
jobs: permissions:
build: contents: read
jobs:
test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version: 24
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Build - name: Build
run: npm run build run: npm run build

View File

@@ -6,24 +6,28 @@ on:
pull_request: pull_request:
branches: [ master ] branches: [ master ]
jobs: permissions:
build: contents: read
jobs:
test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version: 24
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Build - name: Build
run: npm run build run: npm run build
- name: Check status code - name: Check status code
run: npm run test run: npm run test

36
.github/workflows/txxx.yml vendored Normal file
View File

@@ -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

View File

@@ -6,23 +6,29 @@ on:
pull_request: pull_request:
branches: [ master ] branches: [ master ]
jobs: permissions:
build: contents: read
jobs:
test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version: 24
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Build - name: Build
run: npm run build run: npm run build

View File

@@ -6,23 +6,29 @@ on:
pull_request: pull_request:
branches: [ master ] branches: [ master ]
jobs: permissions:
build: contents: read
jobs:
test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version: 24
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Build - name: Build
run: npm run build run: npm run build

View File

@@ -6,23 +6,29 @@ on:
pull_request: pull_request:
branches: [ master ] branches: [ master ]
jobs: permissions:
build: contents: read
jobs:
test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version: 24
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Build - name: Build
run: npm run build run: npm run build

View File

@@ -6,23 +6,29 @@ on:
pull_request: pull_request:
branches: [ master ] branches: [ master ]
jobs: permissions:
build: contents: read
jobs:
test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version: 24
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Build - name: Build
run: npm run build run: npm run build

View File

@@ -1,22 +1,17 @@
# Closing remarks # 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. 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: Core dependencies:
- [express](https://github.com/expressjs/express) - [express](https://github.com/expressjs/express)
- [cheerio](https://cheerio.js.org/) - [cheerio](https://cheerio.js.org/)
- [keyv](https://github.com/jaredwray/keyv) - [keyv](https://github.com/jaredwray/keyv)
# Alternative-links ## Frequently asked questions
Just in case if https://lust.scathach.id down, here some alternative deployment **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.

View File

@@ -1,4 +1,4 @@
FROM node:latest FROM node:22
WORKDIR /srv/app WORKDIR /srv/app
COPY package*.json ./ COPY package*.json ./

180
README.md
View File

@@ -1,14 +1,14 @@
<div align="center"> <div align="center">
<a href="https://lust.scathach.id"><img width="500" src="https://cdn.discordapp.com/attachments/1046495201176334467/1097683154959077496/lust-node_1.png" alt="lustpress"></a> <a href="http://localhost:3000/"><img width="500" src="resources/project/images/lustpress-node_1.png" alt="lustpress"></a>
<h4 align="center">RESTful and experimental API for the PornHub and other adult videos</h4> <h4 align="center">RESTful and experimental API for PornHub and other R18 websites</h4>
<p align="center"> <p align="center">
<a href="https://github.com/sinkaroid/lustpress/actions/workflows/playground.yml"><img src="https://github.com/sinkaroid/lustpress/workflows/Playground/badge.svg"></a> <a href="https://github.com/sinkaroid/lustpress/actions/workflows/playground.yml"><img src="https://github.com/sinkaroid/lustpress/workflows/Playground/badge.svg"></a>
<a href="https://codeclimate.com/github/sinkaroid/lustpress/maintainability"><img src="https://api.codeclimate.com/v1/badges/29a2be78f853f9e3a4a3/maintainability" /></a> <a href="https://codeclimate.com/github/sinkaroid/lustpress/maintainability"><img src="https://api.codeclimate.com/v1/badges/29a2be78f853f9e3a4a3/maintainability" /></a>
</p> </p>
Lustpress stand for Lust and **Express**, rebuild from [Jandapress](https://github.com/sinkaroid/jandapress) with completely different approach. 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.
<a href="https://sinkaroid.github.io/lustpress">Playground</a> • <a href="https://sinkaroid.github.io/lustpress">Playground</a> •
<a href="https://github.com/sinkaroid/lustpress/blob/master/CONTRIBUTING.md">Contributing</a> • <a href="https://github.com/sinkaroid/lustpress/blob/master/CONTRIBUTING.md">Contributing</a> •
@@ -17,49 +17,57 @@ The motivation of this project is to bring you an actionable data related to por
--- ---
<a href="https://lust.scathach.id"><img align="right" src="https://cdn.discordapp.com/attachments/1046495201176334467/1097784363963383848/lp_copy.png" width="320"></a> <a href="http://localhost:3000/"><img align="right" src="resources/project/images/bnnuy.png" width="320"></a>
- [Lustpress](#) - [Lustpress](#)
- [The problem](#the-problem) - [The problem](#the-problem)
- [The solution](#the-solution) - [The solution](#the-solution)
- [Features](#features) - [Features](#features)
- [Lustpress avaibility](#lustpress-avaibility) - [Running tests](#Running-tests)
- [Prerequisites](#prerequisites) - [Prerequisites](#prerequisites)
- [Installation](#installation) - [Installation](#installation)
- [Docker](#docker) - [Docker](#docker)
- [Manual](#manual) - [Manual](#manual)
- [Running tests](#running-tests) - [Tests case](#tests)
- [Playground](https://sinkaroid.github.io/lustpress) - [Playground](https://sinkaroid.github.io/lustpress)
- [Routing](#playground) - [Routing](#playground)
- [Status response](#status-response) - [Status response](#status-response)
- [CLosing remarks](https://github.com/sinkaroid/lustpress/blob/master/CLOSING_REMARKS.md) - [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) - [Alternative links](https://github.com/sinkaroid/lustpress/blob/master/CLOSING_REMARKS.md#alternative-links)
- [Pronunciation](#Pronunciation) - [Pronunciation](#Pronunciation)
- [Client libraries / Wrappers](#client-libraries--wrappers) - [Client libraries](#client-libraries)
- [Acknowledgements](#acknowledgements) - [Acknowledgements](#acknowledgements)
- [Legal](#legal) - [Legal](#legal)
- [Discontinued playground](#frequently-asked-questions)
- [again, discontinued playground](#frequently-asked-questions)
## The problem ## 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 ## 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
<a href="https://sinkaroid.github.io/lustpress"><img src="https://cdn.discordapp.com/attachments/938964058735013899/1097780378061766697/glow.png" width="800"></a> <a href="https://sinkaroid.github.io/lustpress"><img src="resources/project/images/coverage.png" width="800"></a>
## Features ## Features
- Ratelimiting and throttling - Aggregates data from multiple sites.
- Gather the most adult videos on the internet - Provides a consistent and structured response format across all sources.
- Objects taken are consistent structure - Extracted objects are normalized and reassembled to support extensibility.
- Objects taken is re-appended to make extendable - Unified interface supporting **get**, **search**, and **random** methods.
- All in one: get, search, and random methods - Planned support for optional **JWT authentication** in future releases.
- In the future we may implement JWT authentication - Primarily based on pure scraping techniques (with limited exceptions where required).
- Pure scraping, does not hit the API (if exists)
## 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 | | Site | Status | Get | Search | Random | Related |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | ------ | ------ | ------- | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | ------ | ------ | ------- |
| `pornhub` | [![pornhub](https://github.com/sinkaroid/lustpress/workflows/Pornhub%20test/badge.svg)](https://github.com/sinkaroid/lustpress/actions/workflows/pornhub.yml) | `Yes` | `Yes` | `Yes` | `Yes` | | `pornhub` | [![pornhub](https://github.com/sinkaroid/lustpress/workflows/Pornhub%20test/badge.svg)](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` | [![xvideos](https://github.com/sinkaroid/lustpress/workflows/Xvideos%20test/badge.svg)](https://github.com/sinkaroid/lustpress/actions/workflows/xvideos.yml) | `Yes` | `Yes` | `Yes` | `Yes` | | `xvideos` | [![xvideos](https://github.com/sinkaroid/lustpress/workflows/Xvideos%20test/badge.svg)](https://github.com/sinkaroid/lustpress/actions/workflows/xvideos.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
| `xhamster` | [![xhamster](https://github.com/sinkaroid/lustpress/workflows/Xhamster%20test/badge.svg)](https://github.com/sinkaroid/lustpress/actions/workflows/xhamster.yml) | `Yes` | `Yes` | `Yes` | `Yes` | | `xhamster` | [![xhamster](https://github.com/sinkaroid/lustpress/workflows/Xhamster%20test/badge.svg)](https://github.com/sinkaroid/lustpress/actions/workflows/xhamster.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
| `youporn` | [![youporn](https://github.com/sinkaroid/lustpress/workflows/Youporn%20test/badge.svg)](https://github.com/sinkaroid/lustpress/actions/workflows/youporn.yml) | `Yes` | `Yes` | `Yes` | `Yes` | | `youporn` | [![youporn](https://github.com/sinkaroid/lustpress/workflows/Youporn%20test/badge.svg)](https://github.com/sinkaroid/lustpress/actions/workflows/youporn.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
| `eporner` | | `Yes` | `Yes` | `Yes` | `Yes` | | `eporner` | [![Eporner test](https://github.com/sinkaroid/lustpress/actions/workflows/eporner.yml/badge.svg)](https://github.com/sinkaroid/lustpress/actions/workflows/eporner.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
| `txxx` | | `Yes` | `Yes` | `Yes` | `Yes` | | `txxx` | [![Txxx test](https://github.com/sinkaroid/lustpress/actions/workflows/txxx.yml/badge.svg)](https://github.com/sinkaroid/lustpress/actions/workflows/txxx.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
## Prerequisites ## Prerequisites
<table> <table>
<td><b>NOTE:</b> NodeJS 16.x or higher</td> <td><b>NOTE:</b> NodeJS 22.x or higher</td>
</table> </table>
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 `<Buffer>` type. 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 `<Buffer>` type.
@@ -95,7 +103,7 @@ REDIS_URL = redis://default:somenicepassword@redis-666.c10.us-east-6-6.ec666.clo
EXPIRE_CACHE = 1 EXPIRE_CACHE = 1
# you must identify your origin, if not set it will use default # 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 ### Docker
@@ -110,7 +118,7 @@ docker run -d \
-p 3000:3000 \ -p 3000:3000 \
-e REDIS_URL='redis://default:somenicepassword@redis-666.c10.us-east-6-6.ec666.cloud.redislabs.com:1337' \ -e REDIS_URL='redis://default:somenicepassword@redis-666.c10.us-east-6-6.ec666.cloud.redislabs.com:1337' \
-e EXPIRE_CACHE='1' \ -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 ghcr.io/sinkaroid/lustpress:latest
``` ```
@@ -125,8 +133,28 @@ docker run -d \
- Lustpress testing and hot reload - Lustpress testing and hot reload
- `npm run start:dev` - `npm run start:dev`
## Running tests ## Tests
Lustpress testing 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 ### Start the production server
`npm run start:prod` `npm run start:prod`
@@ -134,10 +162,10 @@ Lustpress testing
### Running development server ### Running development server
`npm run start:dev` `npm run start:dev`
### Check the whole sites, It's available for scraping or not ### Generating playground like swagger from apidoc definition
`npm run test` `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 ## Playground
https://sinkaroid.github.io/lustpress https://sinkaroid.github.io/lustpress
@@ -156,10 +184,10 @@ The missing piece of pornhub.com - https://sinkaroid.github.io/lustpress/#api-po
- <u>sort parameters on search</u> - <u>sort parameters on search</u>
- "mr", "mv", "tr", "lg" - "mr", "mv", "tr", "lg"
- Example - Example
- https://lust.scathach.id/pornhub/get?id=ph63c4e1dc48fe7 - http://localhost:3000/pornhub/get?id=ph63c4e1dc48fe7
- https://lust.scathach.id/pornhub/search?key=milf&page=2&sort=mr - http://localhost:3000/pornhub/search?key=milf&page=2&sort=mr
- https://lust.scathach.id/pornhub/related?id=ph63c4e1dc48fe7 - http://localhost:3000/pornhub/related?id=ph63c4e1dc48fe7
- https://lust.scathach.id/pornhub/random - http://localhost:3000/pornhub/random
### Xnxx ### Xnxx
The missing piece of xnxx.com - https://sinkaroid.github.io/lustpress/#api-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
- <u>sort parameters on search</u> - <u>sort parameters on search</u>
- TBD - TBD
- Example - Example
- https://lust.scathach.id/xnxx/get?id=video-17vah71a/makima_y_denji - http://localhost:3000/xnxx/get?id=video-17vah71a/makima_y_denji
- https://lust.scathach.id/xnxx/search?key=bbc&page=2 - http://localhost:3000/xnxx/search?key=bbc&page=2
- https://lust.scathach.id/xnxx/related?id=video-17vah71a/makima_y_denji - http://localhost:3000/xnxx/related?id=video-17vah71a/makima_y_denji
- https://lust.scathach.id/xnxx/random - http://localhost:3000/xnxx/random
### RedTube ### RedTube
The missing piece of redtube.com - https://sinkaroid.github.io/lustpress/#api-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
- <u>sort parameters on search</u> - <u>sort parameters on search</u>
- TBD - TBD
- Example - Example
- https://lust.scathach.id/redtube/get?id=42763661 - http://localhost:3000/redtube/get?id=42763661
- https://lust.scathach.id/redtube/search?key=asian&page=2 - http://localhost:3000/redtube/search?key=asian&page=2
- https://lust.scathach.id/redtube/related?id=42763661 - http://localhost:3000/redtube/related?id=42763661
- https://lust.scathach.id/redtube/random - http://localhost:3000/redtube/random
### Xvideos ### Xvideos
The missing piece of xvideos.com - https://sinkaroid.github.io/lustpress/#api-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
- <u>sort parameters on search</u> - <u>sort parameters on search</u>
- TBD - TBD
- Example - Example
- https://lust.scathach.id/xvideos/get?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_ - http://localhost:3000/xvideos/get?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_
- https://lust.scathach.id/xvideos/search?key=hentai&page=2 - http://localhost:3000/xvideos/search?key=hentai&page=2
- https://lust.scathach.id/xvideos/related?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_ - http://localhost:3000/xvideos/related?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_
- https://lust.scathach.id/xvideos/random - http://localhost:3000/xvideos/random
### Xhamster ### Xhamster
The missing piece of xhamster.com - https://sinkaroid.github.io/lustpress/#api-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 - **search**, takes parameters : `key`, `?page`, and TBD
- **related**, takes parameters : `id` - **related**, takes parameters : `id`
- **random** - **random**
- <u>sort parameters on search</u>
- TBD
- Example - Example
- https://lust.scathach.id/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx - http://localhost:3000/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
- https://lust.scathach.id/xhamster/search?key=arab&page=2 - http://localhost:3000/xhamster/search?key=arab&page=2
- https://lust.scathach.id/xhamster/related?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx - http://localhost:3000/xhamster/related?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
- https://lust.scathach.id/xhamster/random - http://localhost:3000/xhamster/random
### YouPorn ### YouPorn
The missing piece of youporn.com - https://sinkaroid.github.io/lustpress/#api-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
- <u>sort parameters on search</u> - <u>sort parameters on search</u>
- TBD - TBD
- Example - Example
- https://lust.scathach.id/youporn/get?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps - http://localhost:3000/youporn/get?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps
- https://lust.scathach.id/youporn/search?key=teen&page=2 - http://localhost:3000/youporn/search?key=teen&page=2
- https://lust.scathach.id/youporn/related?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps - http://localhost:3000/youporn/related?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps
- https://lust.scathach.id/youporn/random - http://localhost:3000/youporn/random
### JavHD ### Eporner
The missing piece of javhd.com - TBA https://sinkaroid.github.io/lustpress/#api-eporner
- `/javhd` : javhd api - `/eporner` : eporner api
- **get**, takes parameters : TBD - **get**, takes parameters : `id`
- **search**, takes parameters : TBD - **search**, takes parameters : `key`, `?page`
- **related**, takes parameters : TBD - **related**, takes parameters : `id`
- **random** - **random**
- <u>sort parameters on search</u> - <u>sort parameters on search</u>
- TBD - TBD
- Example - 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 ## Status response
@@ -258,23 +300,23 @@ The missing piece of javhd.com - TBA
## Frequently asked questions ## Frequently asked questions
**Q: The website response is slow** **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 ## Pronunciation
`en_US`**/lʌstˈprɛs/** — "lust" stand for this project and "press" for express. `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 Seamlessly integrate with the languages you love, simplified the usage, and intelisense definitions on your IDEs
- TBD - TBD
- Or [create your own](https://github.com/sinkaroid/lustpress/edit/master/README.md) - 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 ## Legal
This tool can be freely copied, modified, altered, distributed without any attribution whatsoever. However, if you feel This tool can be freely copied, modified, altered, distributed without any attribution whatsoever. However, if you feel

45
eslint.config.js Normal file
View File

@@ -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],
},
},
];

View File

@@ -1,7 +1,7 @@
{ {
"name": "lustpress", "name": "lustpress",
"version": "1.6.3-alpha", "version": "8.1.2-alpha",
"description": "RESTful and experimental API for PornHub and other porn sites, which official is lack even isn't exist.", "description": "RESTful and experimental API for PH and other R18 websites",
"main": "build/src/index.js", "main": "build/src/index.js",
"scripts": { "scripts": {
"build": "rimraf build && tsc", "build": "rimraf build && tsc",
@@ -13,20 +13,21 @@
"start:flyctl": "flyctl deploy", "start:flyctl": "flyctl deploy",
"lint": "eslint . --ext .ts", "lint": "eslint . --ext .ts",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"build:freshdoc": "rimraf docs && rimraf template && rimraf theme.zip", "build:freshdoc": "rimraf docs",
"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:freshdoc && npx apidoc -i src -o docs",
"build:apidoc": "npm run build:template && apidoc -i src -o ./docs -t ./template/template-scarlet", "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:pornhub": "start-server-and-test 3000 \"curl -v http://localhost:3000/pornhub/random | jq '.'\"", "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:xnxx": "start-server-and-test 3000 \"curl -v http://localhost:3000/xnxx/random | jq '.'\"", "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:redtube": "start-server-and-test 3000 \"curl -v http://localhost:3000/redtube/random | jq '.'\"", "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:xvideos": "start-server-and-test 3000 \"curl -v http://localhost:3000/xvideos/random | jq '.'\"", "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:xhamster": "start-server-and-test 3000 \"curl -v http://localhost:3000/xhamster/random | jq '.'\"", "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:youporn": "start-server-and-test 3000 \"curl -v http://localhost:3000/youporn/random | jq '.'\"" "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": { "apidoc": {
"title": "Lustpress API Documentation", "title": "Lustpress API Documentation",
"url": "https://lust.scathach.id", "url": "http://localhost:3000",
"sampleUrl": "https://lust.scathach.id", "sampleUrl": "http://localhost:3000",
"name": "Lustpress" "name": "Lustpress"
}, },
"keywords": [], "keywords": [],
@@ -37,34 +38,34 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@keyv/redis": "^2.5.7", "@keyv/redis": "^5.1.6",
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.2.0",
"cors": "^2.8.5", "cors": "^2.8.6",
"dotenv": "^16.0.3", "dotenv": "^17.3.1",
"express": "^4.18.2", "express": "^5.2.1",
"express-rate-limit": "^6.7.0", "express-rate-limit": "^8.3.1",
"express-slow-down": "^1.6.0", "express-slow-down": "^3.1.0",
"keyv": "^4.5.2", "keyv": "^5.6.0",
"phin": "^3.7.0", "phin": "^3.7.1",
"pino": "^8.11.0", "pino": "^10.3.1",
"pino-pretty": "^10.0.0" "pino-pretty": "^13.1.3",
"playwright": "^1.58.2"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.13", "@types/cors": "^2.8.19",
"@types/express": "^4.17.17", "@types/express": "^5.0.6",
"@types/express-slow-down": "^1.3.2", "@types/node": "^25.4.0",
"@types/node": "^18.15.11", "@typescript-eslint/eslint-plugin": "^8.57.0",
"@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/parser": "^8.57.0",
"@typescript-eslint/parser": "^5.58.0", "apidoc": "^1.2.0",
"apidoc": "0.29.0", "eslint": "^10.0.3",
"eslint": "^8.38.0", "rimraf": "^6.1.3",
"rimraf": "^5.0.0", "start-server-and-test": "^2.1.5",
"start-server-and-test": "^2.0.0", "ts-node": "^10.9.2",
"ts-node": "^10.9.1",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"typescript": "5.0.4" "typescript": "5.9.3"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=22"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View File

@@ -1,54 +1,159 @@
import { URL } from "node:url";
import { chromium } from "playwright";
import p, { IResponse } from "phin"; import p, { IResponse } from "phin";
import Keyv from "keyv"; import Keyv from "keyv";
import KeyvRedis from "@keyv/redis";
import pkg from "../package.json"; import pkg from "../package.json";
const keyv = process.env.REDIS_URL
const keyv = new 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)); keyv.on("error", err => console.log("Connection Error", err));
const ttl = 1000 * 60 * 60 * Number(process.env.EXPIRE_CACHE); const ttl = 1000 * 60 * 60 * Number(process.env.EXPIRE_CACHE);
class LustPress { class LustPress {
url: string; url: string;
useragent: string; useragent: string;
private cookieCache: { [domain: string]: string } = {};
constructor() { constructor() {
this.url = ""; 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<string> {
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 * Fetch body from url and check if it's cached
* @param url url to fetch * @param url url to fetch
* @returns Buffer * @returns Buffer
*/ */
async fetchBody(url: string): Promise<Buffer> { async fetchBody(url: string): Promise<Buffer> {
const cached = await keyv.get(url); const cached = await keyv.get(url);
if (cached) { if (cached) {
console.log("Fetching from cache"); console.log("Fetching from cache");
return cached; return cached;
} else if (url.includes("/random")) { } else if (url.includes("/random")) {
console.log("Random should not be cached"); 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, url: url,
"headers": { headers: {
"User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/16.9.1`, "User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/${process.versions.node}`,
}, ...(cookieHeader && { "Cookie": cookieHeader })
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`,
}, },
followRedirects: true 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); await keyv.set(url, res.body, ttl);
return res.body; return res.body;
} }
} }
@@ -110,7 +215,7 @@ class LustPress {
} }
} }
} }
/** /**
* convert seconds to minute * convert seconds to minute
* @param seconds seconds to convert * @param seconds seconds to convert
@@ -143,9 +248,9 @@ class LustPress {
* @returns <Promise<string>> * @returns <Promise<string>>
*/ */
async getServer(): Promise<string> { async getServer(): Promise<string> {
const raw = await p({ const raw = await p({
"url": "http://ip-api.com/json", "url": "http://ip-api.com/json",
"parse": "json" "parse": "json"
}) as IResponse; }) as IResponse;
const data = raw.body as unknown as { country: string, regionName: string }; const data = raw.body as unknown as { country: string, regionName: string };
return `${data.country}, ${data.regionName}`; return `${data.country}, ${data.regionName}`;

View File

@@ -4,9 +4,6 @@ import c from "../../utils/options";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier"; import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express"; import { Request, Response } from "express";
import LustPress from "../../LustPress";
const lust = new LustPress();
export async function randomEporner(req: Request, res: Response) { export async function randomEporner(req: Request, res: Response) {
/** /**

View File

@@ -1,7 +1,7 @@
import { scrapeContent } from "../../scraper/eporner/epornerSearchController"; import { scrapeContent } from "../../scraper/eporner/epornerSearchController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import { maybeError, spacer } from "../../utils/modifier"; import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express"; import { Request, Response } from "express";
export async function searchEporner(req: Request, res: Response) { 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"); if (isNaN(page)) throw Error("Parameter page must be a number");
const slug = key const slug = key
.toLowerCase() .toLowerCase()
.trim() .trim()
.replace(/\s+/g, "-"); .replace(/\s+/g, "-");
const url = const url =
page === 1 page === 1
? `${c.EPORNER}/tag/${slug}/` ? `${c.EPORNER}/tag/${slug}/`
: `${c.EPORNER}/tag/${slug}/${page}/`; : `${c.EPORNER}/tag/${slug}/${page}/`;
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ logger.info({
path: req.path, path: req.path,
query: req.query, query: req.query,
method: req.method, method: req.method,
ip: req.ip, ip: req.ip,
useragent: req.get("User-Agent") useragent: req.get("User-Agent")
}); });
return res.json(data); return res.json(data);
} catch (err) { } catch (err) {
const e = err as Error; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); res.status(400).json(maybeError(false, e.message));
} }
} }

View File

@@ -14,7 +14,7 @@ function getRelatedApiUrl(videoId: string, page = 1, count = 50): string {
const thousand = Math.floor(id / 1_000) * 1_000; const thousand = Math.floor(id / 1_000) * 1_000;
return ( return (
`https://txxx.com/api/json/videos_related2/` + "https://txxx.com/api/json/videos_related2/" +
`432000/${count}/${million}/${thousand}/${id}.all.${page}.json` `432000/${count}/${million}/${thousand}/${id}.all.${page}.json`
); );
} }

View File

@@ -6,53 +6,53 @@ import { logger } from "../../utils/logger";
const lust = new LustPress(); const lust = new LustPress();
export async function randomTxxx(req: Request, res: Response) { export async function randomTxxx(req: Request, res: Response) {
try { try {
const apiUrl = const apiUrl =
"https://txxx.com/api/json/videos2/14400/str/most-popular/60/..1.all..day.json"; "https://txxx.com/api/json/videos2/14400/str/most-popular/60/..1.all..day.json";
const buffer = await lust.fetchBody(apiUrl); const buffer = await lust.fetchBody(apiUrl);
const rawData = JSON.parse(buffer.toString("utf-8")); const rawData = JSON.parse(buffer.toString("utf-8"));
const videos = Array.isArray(rawData.videos) const videos = Array.isArray(rawData.videos)
? rawData.videos ? rawData.videos
: []; : [];
if (videos.length === 0) { if (videos.length === 0) {
throw new Error("No videos returned from upstream"); 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));
} }
// 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));
}
} }

View File

@@ -5,59 +5,59 @@ import { maybeError } from "../../utils/modifier";
const lust = new LustPress(); const lust = new LustPress();
export async function searchTxxx(req: Request, res: Response) { export async function searchTxxx(req: Request, res: Response) {
try { try {
const key = String(req.query.key || "").trim(); const key = String(req.query.key || "").trim();
const page = Number(req.query.page || 1); const page = Number(req.query.page || 1);
if (!key) { if (!key) {
return res.json({ return res.json({
success: false, success: false,
error: "Parameter key is required", error: "Parameter key is required",
}); });
} }
if (Number.isNaN(page)) { if (Number.isNaN(page)) {
return res.json({ return res.json({
success: false, success: false,
error: "Parameter page must be a number", error: "Parameter page must be a number",
}); });
} }
const apiUrl = const apiUrl =
`https://txxx.com/api/videos2.php` + "https://txxx.com/api/videos2.php" +
`?params=259200/str/relevance/60/search..${page}.all..` + `?params=259200/str/relevance/60/search..${page}.all..` +
`&s=${encodeURIComponent(key)}`; `&s=${encodeURIComponent(key)}`;
// Fetch from API directly // Fetch from API directly
const buffer = await lust.fetchBody(apiUrl); const buffer = await lust.fetchBody(apiUrl);
const rawData = JSON.parse(buffer.toString("utf-8")); 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) => ({ const data = videos.map((v: any) => ({
video_id: v.video_id, video_id: v.video_id,
title: v.title, title: v.title,
dir: v.dir, dir: v.dir,
duration: v.duration, duration: v.duration,
views: v.video_viewed, views: v.video_viewed,
rating: v.rating, rating: v.rating,
uploaded: v.post_date, uploaded: v.post_date,
likes: v.likes, likes: v.likes,
dislikes: v.dislikes, dislikes: v.dislikes,
image: v.scr, image: v.scr,
categories: v.categories ? v.categories.split(",") : [], categories: v.categories ? v.categories.split(",") : [],
embed: `https://txxx.com/embed/${v.video_id}/`, embed: `https://txxx.com/embed/${v.video_id}/`,
})); }));
return res.json({ return res.json({
success: true, success: true,
total_count: String(rawData.total_count ?? videos.length), total_count: String(rawData.total_count ?? videos.length),
pages: Number(rawData.pages ?? 1), pages: Number(rawData.pages ?? 1),
page, page,
data, data,
}); });
} catch (err) { } catch (err) {
const e = err as Error; const e = err as Error;
return res.json(maybeError(false, e.message)); return res.json(maybeError(false, e.message));
} }
} }

View File

@@ -49,8 +49,9 @@ export async function scrapeContent(url: string) {
this.upVote = $(".likeup i").first().text().trim() || "0"; this.upVote = $(".likeup i").first().text().trim() || "0";
this.downVote = $(".likedown 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); const jsonLd = JSON.parse(jsonLdText);
// eslint-disable-next-line no-unused-vars
const isoDuration = jsonLd.duration; // "PT00H8M12S" const isoDuration = jsonLd.duration; // "PT00H8M12S"
this.publish = jsonLd.uploadDate || "None"; this.publish = jsonLd.uploadDate || "None";
@@ -62,10 +63,10 @@ export async function scrapeContent(url: string) {
this.video = `https://www.eporner.com/embed/${code}/`; this.video = `https://www.eporner.com/embed/${code}/`;
this.tags = $("li.vit-category a") this.tags = $("li.vit-category a")
.map((_, el) => $(el).text().trim()).get(); .map((_, el) => $(el).text().trim()).get();
this.models = $("li.vit-pornstar a") this.models = $("li.vit-pornstar a")
.map((_, el) => $(el).text().trim()).get(); .map((_, el) => $(el).text().trim()).get();
} }
} }

View File

@@ -5,66 +5,66 @@ import { ISearchItem } from "../../interfaces";
const lust = new LustPress(); const lust = new LustPress();
export async function scrapeContent(url: string) { export async function scrapeContent(url: string) {
try { try {
const res = await lust.fetchBody(url); const res = await lust.fetchBody(url);
const $ = load(res); const $ = load(res);
class EPornerSearch { class EPornerSearch {
data: ISearchItem[]; data: ISearchItem[];
constructor() { constructor() {
this.data = $("#vidresults .mb") this.data = $("#vidresults .mb")
.map((i, el) => { .map((i, el) => {
const link = const link =
$(el).find(".mbimg a").first().attr("href") || ""; $(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) {
if (match[1] === "video-") { if (match[1] === "video-") {
id = `video-${match[2]}`; id = `video-${match[2]}`;
} else { } else {
id = match[2]; 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();
} }
}
const ep = new EPornerSearch(); const code = id.startsWith("video-")
if (ep.data.length === 0) throw Error("No result found"); ? id.replace("video-", "")
: id;
return { return {
success: true, link: `https://www.eporner.com${link}`,
data: ep.data as ISearchItem[], id,
source: url, title: $(el).find(".mbtit a").text().trim(),
}; image: $(el).find(".mbimg img").attr("src"),
} catch (err) { duration: $(el).find(".mbtim").text().trim(),
const e = err as Error; rating: $(el).find(".mbrate").text().trim(),
throw Error(e.message); 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);
}
} }

View File

@@ -12,8 +12,8 @@ export async function scrapeContent(url: string) {
const raw = $("#initials-script").html(); const raw = $("#initials-script").html();
const initials = raw const initials = raw
? JSON.parse( ? JSON.parse(
raw.replace(/^window\.initials\s*=\s*/, "").replace(/;$/, ""), raw.replace(/^window\.initials\s*=\s*/, "").replace(/;$/, ""),
) )
: null; : null;
class Xhamster { class Xhamster {
@@ -47,7 +47,7 @@ export async function scrapeContent(url: string) {
.filter(Boolean); .filter(Boolean);
const videoScript = scripts.find( const videoScript = scripts.find(
(s) => s.includes('"videoModel"') && s.includes('"duration"'), (s) => s.includes("\"videoModel\"") && s.includes("\"duration\""),
); );
if (videoScript) { if (videoScript) {

View File

@@ -64,7 +64,7 @@ export async function scrapeContent(url: string) {
.get(); .get();
this.search = cards this.search = cards
.map((i, el) => { .map((i) => {
return { return {
link: `${c.YOUPORN}${this.links[i]}`, link: `${c.YOUPORN}${this.links[i]}`,
id: this.ids[i], id: this.ids[i],

View File

@@ -10,7 +10,7 @@ const limiter = rateLimit({
const slow = slowDown({ const slow = slowDown({
delayAfter: 50, delayAfter: 50,
windowMs: 15 * 60 * 1000, windowMs: 15 * 60 * 1000,
delayMs: 1000, delayMs: () => 1000,
maxDelayMs: 20000, maxDelayMs: 20000,
}); });

62
test/lustpress.test.ts Normal file
View File

@@ -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");
});

View File

@@ -1,20 +1,50 @@
import test from "node:test";
import assert from "node:assert/strict";
import p from "phin"; import p from "phin";
import { chromium } from "playwright";
import { load } from "cheerio"; 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() { async function getCookies() {
const res = await p({ const browser = await chromium.launch({ headless: true });
url: url, const context = await browser.newContext();
"headers": {
"User-Agent": process.env.USER_AGENT || "lustpress/1.6.0 Node.js/16.9.1", const page = await context.newPage();
}, await page.goto(url, { waitUntil: "domcontentloaded" });
}); await page.waitForLoadState("networkidle");
const $ = load(res.body); const cookies = await context.cookies();
const title = $("meta[property='og:title']").attr("content"); await browser.close();
console.log(title);
console.log(res.statusCode); 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");
});

View File

@@ -18,7 +18,8 @@
"strict": true, "strict": true,
"strictBindCallApply": true, "strictBindCallApply": true,
"strictPropertyInitialization": false, "strictPropertyInitialization": false,
"declaration": true "declaration": true,
"skipLibCheck": true
}, },
"include": [ "include": [
"src/**/*" "src/**/*"