14 Commits

Author SHA1 Message Date
Indrawan I.
67445ed648 fix: resolve cache ttl calculation (#22) 2026-05-14 06:38:19 +07:00
Indrawan I.
e2c59b032b docs: elysia changes & ph fixes (#21) 2026-05-14 05:36:33 +07:00
Indrawan I.
de73724e17 ci: add playground workflows (#20) 2026-05-14 05:15:15 +07:00
Indrawan I.
cd32689b47 feat(core): hard remake with bun and elysia (#19)
* static files changes

* remake modifiers

* test case

* some docs changes

* deps and rule changes

* edit proper interfaces

* remake getServer

* @elysiajs/swagger

* remake tsconfig

* ci changes use bun

* docs changes

* feat(core): hard remake with bun and elysia

* chore(release): 8.2.0-alpha

* auto release

* ci

* update global

* chore(release): 8.2.1-alpha

* ci
2026-05-14 04:57:54 +07:00
Indrawan I.
90d858ae9d fix: dockerized playwright (#18) 2026-03-14 11:52:05 +07:00
Indrawan I.
e63195035d 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
2026-03-13 11:05:40 +07:00
Lame-Fortuna
09b49f1649 feat: add Eporner and Txxx along with minor fixes (#16)
Co-authored-by: Pi Patel <pi.patel@gmail.com>
2026-03-12 17:51:04 +07:00
Indrawan I
ac1a6d82fb ci: sunset workflows for node 14.x, use 16 instead (#7) 2023-04-20 02:40:00 +07:00
Indrawan I
2b682791d2 docs(CLOSING_REMARKS): add Alternative-links url (#6) 2023-04-20 02:29:18 +07:00
Indrawan I
d2765c8e34 fix(removeHtmlTagWithoutSpace): proper multiple tags slicing (#5)
* removeHtmlTagWithoutSpace

* pre release
2023-04-20 02:14:52 +07:00
Indrawan I
55e88b57d1 fix: proper null type for video, assets and slice slash on fetchBody (#4)
* broken slash will broke it

* xhamster fix embed assets

* xnxx fix nullable assets

* xvideos fix nullable assets

* it should be none instead null type

* pre release
2023-04-19 21:40:53 +07:00
Indrawan I
5f641096a2 chore(release): 1.6.1-alpha, flyctl deploy
chore(release): 1.6.1-alpha, flyctl deploy
2023-04-18 14:47:19 +07:00
Indrawan I
fcf2cff58b ci: add separate testing cases for each site
ci: add separate testing cases for each site
2023-04-18 13:13:28 +07:00
Indrawan I
0f88b3ce5b ci: tdd
ci: tdd
2023-04-18 11:36:31 +07:00
88 changed files with 3098 additions and 2342 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 ]
permissions:
contents: read
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with:
bun-version: latest
- name: Install dependencies - name: Install dependencies
run: npm install run: bun install
- name: Check lint - name: Check lint
run: npm run lint run: bun run lint
- name: Build - name: Build
run: npm run build run: bun 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- 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

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

@@ -0,0 +1,33 @@
name: Eporner test
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run test
run: |
bun run start:dev &
sleep 3
bun run test:eporner

View File

@@ -3,24 +3,64 @@ name: Playground
on: on:
push: push:
branches: [ master ] branches: [ master ]
pull_request:
branches: [ master ] permissions:
contents: write
jobs: jobs:
build-and-deploy: deploy:
concurrency: ci-${{ github.ref }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@v3
- name: Installing dependencies
run: npm install
- name: Generating docs
run: npm run build:apidoc
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4.4.1
with: with:
branch: gh-pages persist-credentials: false
folder: docs
- name: Setup Bun
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Generate Swagger JSON and HTML
run: |
mkdir public
bun run start:dev &
sleep 3
curl -s http://localhost:3000/docs/json > public/swagger.json
cat << 'EOF' > public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>LustPress API Playground</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
<style>body { margin: 0; padding: 0; } .swagger-ui .topbar { display: none; }</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: "./swagger.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
});
};
</script>
</body>
</html>
EOF
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
publish_branch: gh-pages

View File

@@ -6,25 +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: [14.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Pornhub test - name: Setup Bun
run: npm run test:pornhub uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run test
run: |
bun run start:dev &
sleep 3
bun run test:pornhub

View File

@@ -6,25 +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: [14.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Redtube test - name: Setup Bun
run: npm run test:redtube uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run test
run: |
bun run start:dev &
sleep 3
bun run test:redtube

56
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Release
on:
push:
branches: [master]
permissions:
contents: write
pull-requests: read
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
persist-credentials: false
fetch-depth: 0
- name: Set release metadata
id: meta
shell: bash
run: |
VERSION="$(jq -r .version package.json)"
TAG="v${VERSION}"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
- name: Check if tag already exists
id: tag_check
shell: bash
run: |
if git ls-remote --tags origin "refs/tags/${{ steps.meta.outputs.tag }}" | grep -q "${{ steps.meta.outputs.tag }}"; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Create GitHub release
if: steps.tag_check.outputs.exists == 'false'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.meta.outputs.tag }}
name: ${{ steps.meta.outputs.tag }}
target_commitish: master
generate_release_notes: true
- name: Skip release when tag exists
if: steps.tag_check.outputs.exists == 'true'
run: echo "Tag ${{ steps.meta.outputs.tag }} already exists. Skipping release creation."

View File

@@ -6,24 +6,31 @@ 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: [14.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with:
bun-version: latest
- name: Install dependencies - name: Install dependencies
run: npm install run: bun install
- name: Build - name: Build
run: npm run build run: bun run build
- name: Check status code - name: Check status code
run: npm run test run: |
bun run start:dev &
sleep 3
bun run test

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

@@ -0,0 +1,33 @@
name: Txxx test
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run test
run: |
bun run start:dev &
sleep 3
bun run test:txxx

View File

@@ -6,25 +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: [14.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Xhamster test - name: Setup Bun
run: npm run test:xhamster uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run test
run: |
bun run start:dev &
sleep 3
bun run test:xhamster

View File

@@ -6,25 +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: [14.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Xnxx test - name: Setup Bun
run: npm run test:xnxx uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run test
run: |
bun run start:dev &
sleep 3
bun run test:xnxx

View File

@@ -6,25 +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: [14.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Xvideos test - name: Setup Bun
run: npm run test:xvideos uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run test
run: |
bun run start:dev &
sleep 3
bun run test:xvideos

View File

@@ -6,25 +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: [14.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} persist-credentials: false
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Youporn test - name: Setup Bun
run: npm run test:youporn uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run test
run: |
bun run start:dev &
sleep 3
bun run test:youporn

3
.gitignore vendored
View File

@@ -9,4 +9,5 @@ p.ts
.env .env
.idea .idea
CHANGELOG.md CHANGELOG.md
theme.zip theme.zip
AGENTS.md

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) - [elysia](https://elysiajs.com/)
- [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.
- TBA > **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,9 +1,26 @@
FROM node:latest FROM oven/bun:1.3.13-alpine AS base
WORKDIR /srv/app WORKDIR /srv/app
COPY package*.json ./
RUN npm install # 1. Install production dependencies only
FROM base AS deps
COPY package.json bun.lock ./
RUN bun install --production
# 2. Builder stage to compile TypeScript
FROM base AS builder
COPY package.json bun.lock ./
RUN bun install
COPY . . COPY . .
RUN npm run build RUN bun run build
# 3. Final release stage
FROM base AS release
COPY --from=deps /srv/app/node_modules ./node_modules
COPY --from=builder /srv/app/build ./build
COPY package.json ./
# Run as non-root user
USER bun
EXPOSE 3000 EXPOSE 3000
CMD ["node", "build/src/index.js"]
CMD ["bun", "run", "start"]

219
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 was originally named Lust and Express (legacy name) and now runs on **Bun** + **Elysia**.
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 carry 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,56 @@ 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/nun.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](#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)
- [Ph Challenge Solver](#pornhub-js-challenge-solver)
- [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)
- [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,10 +75,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` | [![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` | [![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 14.x or higher</td> <td><b>NOTE:</b> Bun 1.3.13 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.
@@ -80,9 +89,6 @@ To handle several requests from each website, You will also need [Redis](https:/
Rename `.env.schema` to `.env` and fill the value with your own Rename `.env.schema` to `.env` and fill the value with your own
```bash ```bash
# railway, fly.dev, heroku, vercel or any free service
RAILWAY = sinkaroid
# default port # default port
PORT = 3000 PORT = 3000
@@ -93,7 +99,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.2.3-alpha Bun/1.3.13"
``` ```
### Docker ### Docker
@@ -101,14 +107,14 @@ USER_AGENT = "lustpress/1.6.1 Node.js/16.9.1"
docker pull ghcr.io/sinkaroid/lustpress:latest docker pull ghcr.io/sinkaroid/lustpress:latest
docker run -p 3000:3000 -d ghcr.io/sinkaroid/lustpress:latest docker run -p 3000:3000 -d ghcr.io/sinkaroid/lustpress:latest
### Docker (your own) ### Docker (adjust your own)
```bash ```bash
docker run -d \ docker run -d \
--name=lustpress \ --name=lustpress \
-p 3000:3000 \ -p 3028: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.2.3-alpha Bun/1.3.13' \
ghcr.io/sinkaroid/lustpress:latest ghcr.io/sinkaroid/lustpress:latest
``` ```
@@ -117,25 +123,44 @@ docker run -d \
git clone https://github.com/sinkaroid/lustpress.git git clone https://github.com/sinkaroid/lustpress.git
- Install dependencies - Install dependencies
- `npm install / yarn install` - `bun install`
- Lustpress production - Lustpress production
- `npm run start:prod` - `bun run start:prod`
- Lustpress testing and hot reload - Lustpress testing and hot reload
- `npm run start:dev` - `bun run start:dev`
## Tests
Run the following commands to execute tests for each supported source:
```bash
# Check whether all supported sites are available for scraping
bun run test
# Check whether ph and (maybe the others do..) do Solving challenge in their website
bun run test:mock
# Run tests for individual sources
bun run test:pornhub
bun run test:xnxx
bun run test:redtube
bun run test:xvideos
bun run test:xhamster
bun run test:youporn
bun run test:eporner
bun run test:txxx
```
## Running tests
Lustpress testing
### Start the production server ### Start the production server
`npm run start:prod` `bun run start:prod`
### Running development server ### Running development server
`npm run start:dev` `bun run start:dev`
### Check the whole sites, It's available for scraping or not ### Generating playground like swagger from apidoc definition
`npm run test` `bun 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
@@ -154,10 +179,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
@@ -169,10 +194,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
@@ -184,10 +209,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
@@ -199,10 +224,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
@@ -211,13 +236,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
@@ -229,22 +252,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
@@ -254,27 +293,35 @@ The missing piece of javhd.com - TBA
HTTP/1.1 400 Bad Request HTTP/1.1 400 Bad Request
HTTP/1.1 500 Fail to get data HTTP/1.1 500 Fail to get data
## Pornhub JS Challenge Solver
Pornhub serves a JavaScript challenge page to detect automated requests. Instead of relying on a headless browser (Playwright/Puppeteer), Lustpress solves this natively with zero external dependencies.
**How it works:**
- Fetch the index page and capture initial session cookies
- Detect the `leastFactor` math challenge in the response HTML
- Parse obfuscated variables (`p`, `s`) and conditional bitwise operations
- Compute the correct `KEY` cookie value
- Verify the solved cookie against the index
- Cache and reuse cookies for subsequent requests, auto-refresh on expiry
This approach runs in **< 1ms** compared to 5-10s with a headless browser, uses virtually no memory, and is fully **Docker-friendly** — no Chromium installation required.
The solver logic lives in [`src/utils/ph-solver.ts`](src/utils/ph-solver.ts). If Pornhub changes its obfuscation pattern, update the regex patterns in that file.
## 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
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 ## 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
like this tool deserves an attribution, mention it. It won't hurt anybody. like this tool deserves an attribution, mention it. It won't hurt anybody.
> Licence: WTF. > Licence: WTF.

365
bun.lock Normal file
View File

@@ -0,0 +1,365 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "lustpress",
"dependencies": {
"@elysiajs/cors": "^1.2.0",
"@elysiajs/swagger": "^1.2.2",
"@keyv/redis": "^5.1.6",
"cheerio": "^1.2.0",
"elysia": "^1.2.24",
"keyv": "^5.6.0",
},
"devDependencies": {
"@types/bun": "^1.3.13",
"@typescript-eslint/eslint-plugin": "^8.24.1",
"@typescript-eslint/parser": "^8.24.1",
"eslint": "^9.20.1",
"typescript": "^6.0.3",
},
},
},
"packages": {
"@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="],
"@elysiajs/cors": ["@elysiajs/cors@1.4.2", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-FTCcbH35brTLigF1W7BYySRZomgI/dBEMK9BgK9RP9Nez7zmpGh4koL/Yr1BFv8nYz7CfhRvcM8d/c+XnwMaVQ=="],
"@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
"@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
"@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
"@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@keyv/redis": ["@keyv/redis@5.1.6", "", { "dependencies": { "@redis/client": "^5.10.0", "cluster-key-slot": "^1.1.2", "hookified": "^1.13.0" }, "peerDependencies": { "keyv": "^5.6.0" } }, "sha512-eKvW6pspvVaU5dxigaIDZr635/Uw6urTXL3gNbY9WTR8d3QigZQT+r8gxYSEOsw4+1cCBsC4s7T2ptR0WC9LfQ=="],
"@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="],
"@redis/client": ["@redis/client@5.12.1", "", { "dependencies": { "cluster-key-slot": "1.1.2" }, "peerDependencies": { "@node-rs/xxhash": "^1.1.0", "@opentelemetry/api": ">=1 <2" }, "optionalPeers": ["@node-rs/xxhash", "@opentelemetry/api"] }, "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA=="],
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="],
"@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="],
"@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.3", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/type-utils": "8.59.3", "@typescript-eslint/utils": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.3", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.3", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.3", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.3", "@typescript-eslint/types": "^8.59.3", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3" } }, "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.3", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/utils": "8.59.3", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.3", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.3", "@typescript-eslint/tsconfig-utils": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg=="],
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="],
"cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="],
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
"elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="],
"encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"file-type": ["file-type@22.0.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0" } }, "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
"hookified": ["hookified@1.15.1", "", {}, "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg=="],
"htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="],
"parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
"undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="],
"undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"flat-cache/keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
}
}

56
eslint.config.js Normal file
View File

@@ -0,0 +1,56 @@
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",
Bun: "readonly",
fetch: "readonly",
describe: "readonly",
test: "readonly",
expect: "readonly",
beforeAll: "readonly",
afterAll: "readonly",
it: "readonly",
AbortController: "readonly",
setTimeout: "readonly",
clearTimeout: "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,39 +0,0 @@
# fly.toml file generated for lust on 2023-04-18T12:01:37+07:00
app = "lust"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []
[env]
[experimental]
auto_rollback = true
[[services]]
autostart = true
autostop = false
http_checks = []
internal_port = 3000
processes = ["app"]
protocol = "tcp"
script_checks = []
[services.concurrency]
hard_limit = 25
soft_limit = 20
type = "connections"
[[services.ports]]
force_https = true
handlers = ["http"]
port = 80
[[services.ports]]
handlers = ["tls", "http"]
port = 443
[[services.tcp_checks]]
grace_period = "1s"
interval = "15s"
restart_limit = 0
timeout = "2s"

View File

@@ -1,33 +1,24 @@
{ {
"name": "lustpress", "name": "lustpress",
"version": "1.6.1-alpha", "version": "8.2.4-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": "src/index.ts",
"scripts": { "scripts": {
"build": "rimraf build && tsc", "start:dev": "bun --watch src/index.ts",
"start": "node build/src/index.js", "build": "bun build ./src/index.ts --outdir ./build --target bun",
"test": "ts-node test/test.ts", "start": "bun ./build/index.js",
"test:mock": "ts-node test/mock.ts", "start:prod": "bun run build && bun ./build/index.js",
"start:prod": "npm run build && node build/src/index.js", "test": "bun test test/lustpress.test.ts",
"start:dev": "ts-node-dev src/index.ts", "test:pornhub": "bun test -t pornhub test/lustpress.test.ts",
"start:flyctl": "flyctl deploy", "test:xnxx": "bun test -t xnxx test/lustpress.test.ts",
"lint": "eslint . --ext .ts", "test:redtube": "bun test -t redtube test/lustpress.test.ts",
"lint:fix": "eslint . --fix", "test:xvideos": "bun test -t xvideos test/lustpress.test.ts",
"build:freshdoc": "rimraf docs && rimraf template && rimraf theme.zip", "test:xhamster": "bun test -t xhamster test/lustpress.test.ts",
"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", "test:youporn": "bun test -t youporn test/lustpress.test.ts",
"build:apidoc": "npm run build:template && apidoc -i src -o ./docs -t ./template/template-scarlet", "test:eporner": "bun test -t eporner test/lustpress.test.ts",
"test:pornhub": "start-server-and-test 3000 \"curl -v http://localhost:3000/pornhub/random | jq '.'\"", "test:txxx": "bun test -t txxx test/lustpress.test.ts",
"test:xnxx": "start-server-and-test 3000 \"curl -v http://localhost:3000/xnxx/random | jq '.'\"", "lint": "eslint .",
"test:redtube": "start-server-and-test 3000 \"curl -v http://localhost:3000/redtube/random | jq '.'\"", "lint:fix": "eslint . --fix"
"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 '.'\""
},
"apidoc": {
"title": "Lustpress API Documentation",
"url": "https://lust.scathach.id",
"sampleUrl": "https://lust.scathach.id",
"name": "Lustpress"
}, },
"keywords": [], "keywords": [],
"author": "sinkaroid", "author": "sinkaroid",
@@ -37,34 +28,22 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@keyv/redis": "^2.5.7", "@elysiajs/cors": "^1.2.0",
"cheerio": "^1.0.0-rc.12", "@elysiajs/swagger": "^1.2.2",
"cors": "^2.8.5", "@keyv/redis": "^5.1.6",
"dotenv": "^16.0.3", "cheerio": "^1.2.0",
"express": "^4.18.2", "elysia": "^1.2.24",
"express-rate-limit": "^6.7.0", "keyv": "^5.6.0"
"express-slow-down": "^1.6.0",
"keyv": "^4.5.2",
"phin": "^3.7.0",
"pino": "^8.11.0",
"pino-pretty": "^10.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.13", "@types/bun": "^1.3.13",
"@types/express": "^4.17.17", "@typescript-eslint/eslint-plugin": "^8.24.1",
"@types/express-slow-down": "^1.3.2", "@typescript-eslint/parser": "^8.24.1",
"@types/node": "^18.15.11", "eslint": "^9.20.1",
"@typescript-eslint/eslint-plugin": "^5.58.0", "typescript": "^6.0.3"
"@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",
"ts-node-dev": "^2.0.0",
"typescript": "5.0.4"
}, },
"engines": { "engines": {
"node": ">=14" "bun": ">=1.3.13"
} },
} "packageManager": "bun@1.3.13"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,96 +1,139 @@
import p, { IResponse } from "phin"; import { URL } from "node:url";
import Keyv from "keyv"; import Keyv from "keyv";
import KeyvRedis from "@keyv/redis";
import pkg from "../package.json"; import pkg from "../package.json";
import { solveChallenge } from "./utils/ph-solver";
const keyv = process.env.REDIS_URL
? new Keyv({ store: new KeyvRedis(process.env.REDIS_URL) })
: new Keyv();
const keyv = new Keyv(process.env.REDIS_URL); keyv.on("error", (err) => console.log("Connection Error", err));
const ttl = 1000 * 60 * 60 * Number(process.env.EXPIRE_CACHE || "1");
keyv.on("error", err => console.log("Connection Error", err)); const GEO_TIMEOUT_MS = 3000;
const ttl = 1000 * 60 * 60 * Number(process.env.EXPIRE_CACHE); let cachedLastLocation: string | null = null;
let lastLocationTimestamp = 0;
function cachedLocationOrUnknown(): string {
if (cachedLastLocation && Date.now() - lastLocationTimestamp < 3600_000) {
return cachedLastLocation;
}
return "Unknown, Unknown";
}
class LustPress { class LustPress {
url: string; url: string;
useragent: string; useragent: string;
private browserUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36";
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} Bun/${Bun.version}`;
}
async getPornhubCookies(): Promise<string> {
const baseUrl = "https://www.pornhub.com/";
const headers = {
"User-Agent": this.browserUA,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1"
};
console.log("Fetching Pornhub Index for cookies...");
let res = await fetch(baseUrl, { headers, redirect: "follow" });
// Get cookies from set-cookie headers
const initialCookies = res.headers.getSetCookie().map(c => c.split(";")[0]).join("; ");
let text = await res.text();
if (text.includes("leastFactor")) {
console.log("Pornhub Challenge detected at Index. Solving...");
const challengeCookie = await solveChallenge(text);
const combinedCookies = [initialCookies, challengeCookie, "age_verified=1"].filter(Boolean).join("; ");
// Verify cookies
console.log("Verifying cookies at Index...");
res = await fetch(baseUrl, {
headers: { ...headers, Cookie: combinedCookies },
redirect: "follow"
});
text = await res.text();
if (!text.includes("leastFactor")) {
console.log("Pornhub cookies verified!");
return combinedCookies;
} else {
console.log("Warning: Challenge still present after solve attempt.");
return combinedCookies;
}
}
return initialCookies || "age_verified=1";
} }
/**
* Fetch body from url and check if it's cached
* @param url url to fetch
* @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(`[CACHE HIT] ${url}`);
return cached; return cached;
} else if (url.includes("/random")) {
console.log("Random should not be cached");
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");
const res = await p({
url: url,
"headers": {
"User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/16.9.1`,
},
followRedirects: true
});
await keyv.set(url, res.body, ttl);
return res.body;
} }
const isPornhub = /pornhub\.com/i.test(url);
const domain = new URL(url).hostname;
const headers: Record<string, string> = {
"User-Agent": isPornhub ? this.browserUA : this.useragent,
};
if (isPornhub) {
if (!this.cookieCache[domain]) {
this.cookieCache[domain] = await this.getPornhubCookies();
}
headers["Cookie"] = this.cookieCache[domain];
headers["Referer"] = "https://www.pornhub.com/";
headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8";
headers["Accept-Language"] = "en-US,en;q=0.9";
}
console.log(`[FETCH] ${url}`);
let res = await fetch(url, { headers, redirect: "follow" });
let text = await res.text();
if (isPornhub && text.includes("leastFactor")) {
console.log("Challenge detected at target URL. Re-authenticating...");
this.cookieCache[domain] = await this.getPornhubCookies();
res = await fetch(url, {
headers: { ...headers, Cookie: this.cookieCache[domain] },
redirect: "follow"
});
text = await res.text();
}
const body = Buffer.from(text);
if (!url.includes("/random")) await keyv.set(url, body, ttl);
return body;
} }
/** // Utility methods
* remove html tag and bunch of space
* @param str string to remove html tag
* @returns string
*/
removeHtmlTag(str: string): string { removeHtmlTag(str: string): string {
str = str.replace(/(\r\n|\n|\r)/gm, ""); return str.replace(/(\r\n|\n|\r)/gm, "").replace(/\s+/g, "");
str = str.replace(/\s+/g, "");
return str;
} }
/**
* remove html tag without space
* @param str string to remove html tag
* @returns string
*/
removeHtmlTagWithoutSpace(str: string): string { removeHtmlTagWithoutSpace(str: string): string {
str = str.replace(/(\r\n|\n|\r|\t)/gm, ""); return str.replace(/(\r\n|\n|\r|\t)/gm, "").replace(/\\/g, "").replace(/\s+/g, " ").trim();
str = str.replace(/\\/g, "");
return str.trim();
} }
/**
* remove all single quote on array
* @param arr array to remove single quote
* @returns string[]
*/
removeAllSingleQuoteOnArray(arr: string[]): string[] { removeAllSingleQuoteOnArray(arr: string[]): string[] {
return arr.map((item) => item.replace(/'/g, "")); return arr.map((item) => item.replace(/'/g, ""));
} }
/**
* time ago converter
* @param input date to convert
* @returns string
*/
timeAgo(input: Date) { timeAgo(input: Date) {
const date = new Date(input); const date = new Date(input);
const formatter: any = new Intl.RelativeTimeFormat("en"); const formatter = new Intl.RelativeTimeFormat("en");
const ranges: { [key: string]: number } = { const ranges: { [key: string]: number } = {
years: 3600 * 24 * 365, years: 3600 * 24 * 365,
months: 3600 * 24 * 30, months: 3600 * 24 * 30,
@@ -98,56 +141,73 @@ class LustPress {
days: 3600 * 24, days: 3600 * 24,
hours: 3600, hours: 3600,
minutes: 60, minutes: 60,
seconds: 1 seconds: 1,
}; };
const secondsElapsed = (date.getTime() - Date.now()) / 1000; const secondsElapsed = (date.getTime() - Date.now()) / 1000;
for (const key in ranges) { for (const key in ranges) {
if (ranges[key] < Math.abs(secondsElapsed)) { if (ranges[key] < Math.abs(secondsElapsed)) {
const delta = secondsElapsed / ranges[key]; const delta = secondsElapsed / ranges[key];
return formatter.format(Math.round(delta), key); return formatter.format(Math.round(delta), key as Intl.RelativeTimeFormatUnit);
} }
} }
} }
/**
* convert seconds to minute
* @param seconds seconds to convert
* @returns string
*/
secondToMinute(seconds: number): string { secondToMinute(seconds: number): string {
const minutes = Math.floor(seconds / 60); const minutes = Math.floor(seconds / 60);
const second = seconds % 60; const second = seconds % 60;
return `${minutes}min, ${second}sec`; return `${minutes}min, ${second}sec`;
} }
/**
* get current process memory usage
* @returns object
*/
currentProccess() { currentProccess() {
const arr = [1, 2, 3, 4, 5, 6, 9, 7, 8, 9, 10];
arr.reverse();
const rss = process.memoryUsage().rss / 1024 / 1024; const rss = process.memoryUsage().rss / 1024 / 1024;
const heap = process.memoryUsage().heapUsed / 1024 / 1024; const heap = process.memoryUsage().heapUsed / 1024 / 1024;
const heaptotal = process.memoryUsage().heapTotal / 1024 / 1024; const heaptotal = process.memoryUsage().heapTotal / 1024 / 1024;
return { return {
rss: `${Math.round(rss * 100) / 100} MB`, rss: `${Math.round(rss * 100) / 100} MB`,
heap: `${Math.round(heap * 100) / 100}/${Math.round(heaptotal * 100) / 100} MB` heap: `${Math.round(heap * 100) / 100}/${Math.round(heaptotal * 100) / 100} MB`,
}; };
} }
/**
* fetch this server location
* @returns <Promise<string>>
*/
async getServer(): Promise<string> { async getServer(): Promise<string> {
const raw = await p({ const controller = new AbortController();
"url": "http://ip-api.com/json", const timeoutId = setTimeout(() => controller.abort(), GEO_TIMEOUT_MS);
"parse": "json"
}) as IResponse; try {
const data = raw.body as unknown as { country: string, regionName: string }; // ip-api free tier often rejects HTTPS requests with 403;
return `${data.country}, ${data.regionName}`; const raw = await fetch("https://ipwho.is/", {
signal: controller.signal,
});
if (!raw.ok) {
return cachedLocationOrUnknown();
}
const data = await raw.json() as {
success?: boolean;
country?: string;
region?: string;
};
if (data.success === false) {
return cachedLocationOrUnknown();
}
const country = data.country?.trim();
const region = data.region?.trim();
if (!country || !region) {
return cachedLocationOrUnknown();
}
const location = `${country}, ${region}`;
cachedLastLocation = location;
lastLocationTimestamp = Date.now();
return location;
} catch {
return cachedLocationOrUnknown();
} finally {
clearTimeout(timeoutId);
if (!controller.signal.aborted) {
controller.abort();
}
}
} }
} }
export const lust = new LustPress();
export default LustPress; export default LustPress;

View File

@@ -0,0 +1,24 @@
import { scrapeContent } from "../../scraper/eporner/epornerGetController";
import c from "../../utils/options";
export async function getEporner({ query }: { query: { id: string } }) {
try {
const { id } = query;
let path: string;
if (id.startsWith("video-")) {
path = id;
} else {
path = `hd-porn/${id}`;
}
const url = `${c.EPORNER}/${path}`;
const data = await scrapeContent(url);
return data;
} catch (err) {
const e = err as Error;
throw new Error(e.message);
}
}

View File

@@ -0,0 +1,15 @@
import { scrapeContent } from "../../scraper/eporner/epornerGetRelatedController";
import c from "../../utils/options";
export async function relatedEporner({ query }: { query: { id: string } }) {
try {
const { id } = query;
const url = `${c.EPORNER}/video-${id}`;
const data = await scrapeContent(url);
return data;
} catch (err) {
const e = err as Error;
throw new Error(e.message);
}
}

View File

@@ -0,0 +1,32 @@
import { scrapeContent as searchScrape } from "../../scraper/eporner/epornerSearchController";
import { scrapeContent as videoScrape } from "../../scraper/eporner/epornerGetController";
import c from "../../utils/options";
export async function randomEporner() {
try {
const weeklyUrl = `${c.EPORNER}/cat/all/SORT-top-weekly/`;
const list = await searchScrape(weeklyUrl);
if (!list.data.length) {
throw new Error("No weekly top videos found");
}
const random = list.data[Math.floor(Math.random() * list.data.length)];
let path: string;
if (random.id.startsWith("video-")) {
path = random.id;
} else {
path = `hd-porn/${random.id}`;
}
const url = `${c.EPORNER}/${path}`;
const data = await videoScrape(url);
return data;
} catch (err) {
const e = err as Error;
throw new Error(e.message);
}
}

View File

@@ -0,0 +1,29 @@
import { scrapeContent } from "../../scraper/eporner/epornerSearchController";
import c from "../../utils/options";
export async function searchEporner({
query,
}: {
query: { key: string; page?: string };
}) {
try {
const { key } = query;
const page = Number(query.page || 1);
const slug = key
.toLowerCase()
.trim()
.replace(/\s+/g, "-");
const url =
page === 1
? `${c.EPORNER}/tag/${slug}/`
: `${c.EPORNER}/tag/${slug}/${page}/`;
const data = await scrapeContent(url);
return data;
} catch (err) {
const e = err as Error;
throw new Error(e.message);
}
}

View File

@@ -1,55 +1,15 @@
import { scrapeContent } from "../../scraper/pornhub/pornhubGetController"; import { scrapeContent } from "../../scraper/pornhub/pornhubGetController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
export async function getPornhub(req: Request, res: Response) { export async function getPornhub({ query }: { query: { id: string } }) {
try { try {
const id = req.query.id as string; const { id } = query;
if (!id) throw Error("Parameter id is required");
/**
* @api {get} /pornhub/get?id=:id Get Pornhub
* @apiName Get pornhub
* @apiGroup pornhub
* @apiDescription Get a pornhub video based on id
*
* @apiParam {String} id Video ID
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/pornhub/get?id=ph63c4e1dc48fe7
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/pornhub/get?id=ph63c4e1dc48fe7")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/pornhub/get?id=ph63c4e1dc48fe7") as resp:
* print(await resp.json())
*/
const url = `${c.PORNHUB}/view_video.php?viewkey=${id}`; const url = `${c.PORNHUB}/view_video.php?viewkey=${id}`;
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,55 +1,15 @@
import { scrapeContent } from "../../scraper/pornhub/pornhubSearchController"; import { scrapeContent } from "../../scraper/pornhub/pornhubSearchController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
export async function relatedPornhub(req: Request, res: Response) { export async function relatedPornhub({ query }: { query: { id: string } }) {
try { try {
const id = req.query.id as string; const { id } = query;
if (!id) throw Error("Parameter id is required");
/**
* @api {get} /pornhub/get?id=:id Get Pornhub related videos
* @apiName Get pornhub related videos
* @apiGroup pornhub
* @apiDescription Get a related pornhub videos based on id
*
* @apiParam {String} id Video ID
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/pornhub/get?id=ph63c4e1dc48fe7
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/pornhub/get?id=ph63c4e1dc48fe7")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/pornhub/get?id=ph63c4e1dc48fe7") as resp:
* print(await resp.json())
*/
const url = `${c.PORNHUB}/view_video.php?viewkey=${id}`; const url = `${c.PORNHUB}/view_video.php?viewkey=${id}`;
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,50 +1,14 @@
import { Request, Response } from "express";
import { scrapeContent } from "../../scraper/pornhub/pornhubGetController"; import { scrapeContent } from "../../scraper/pornhub/pornhubGetController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
export async function randomPornhub(req: Request, res: Response) { export async function randomPornhub() {
try { try {
/**
* @api {get} /pornhub/random Random pornhub video
* @apiName Random pornhub
* @apiGroup pornhub
* @apiDescription Gets random pornhub video
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/pornhub/random
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/pornhub/random")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/pornhub/random") as resp:
* print(await resp.json())
*
*/
const url = `${c.PORNHUB}/video/random`; const url = `${c.PORNHUB}/video/random`;
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,66 +1,29 @@
import { scrapeContent } from "../../scraper/pornhub/pornhubSearchController"; import { scrapeContent } from "../../scraper/pornhub/pornhubSearchController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger"; import { spacer } from "../../utils/modifier";
import { maybeError, spacer } from "../../utils/modifier";
import { Request, Response } from "express"; const sorting = ["mr", "mv", "tr", "lg"];
const sorting = ["mr", "mv", "tr", "lg"];
export async function searchPornhub({
export async function searchPornhub(req: Request, res: Response) { query,
try { }: {
/** query: { key: string; page?: string; sort?: string };
* @api {get} /pornhub/search Search pornhub videos }) {
* @apiName Search pornhub try {
* @apiGroup pornhub const { key, sort } = query;
* @apiDescription Search pornhub videos const page = query.page || "1";
* @apiParam {String} key Keyword to search
* @apiParam {Number} [page=1] Page number let url;
* @apiParam {String} [sort=mr] Sort by if (!sort) url = `${c.PORNHUB}/video/search?search=${spacer(key)}`;
* else if (!sorting.includes(sort))
* @apiSuccessExample {json} Success-Response: url = `${c.PORNHUB}/video/search?search=${spacer(key)}&page=${page}`;
* HTTP/1.1 200 OK else
* HTTP/1.1 400 Bad Request url = `${c.PORNHUB}/video/search?search=${spacer(key)}&o=${sort}&page=${page}`;
*
* @apiExample {curl} curl const data = await scrapeContent(url);
* curl -i https://lust.scathach.id/pornhub/search?key=milf return data;
* curl -i https://lust.scathach.id/pornhub/search?key=milf&page=2&sort=mr } catch (err) {
* const e = err as Error;
* @apiExample {js} JS/TS throw new Error(e.message);
* import axios from "axios" }
* }
* axios.get("https://lust.scathach.id/pornhub/search?key=milf")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/pornhub/search?key=milf") as resp:
* print(await resp.json())
*/
const key = req.query.key as string;
const page = req.query.page || 1;
const sort = req.query.sort as string;
if (!key) throw Error("Parameter key is required");
if (isNaN(Number(page))) throw Error("Parameter page must be a number");
let url;
if (!sort) url = `${c.PORNHUB}/video/search?search=${spacer(key)}`;
else if (!sorting.includes(sort)) url = `${c.PORNHUB}/video/search?search=${spacer(key)}&page=${page}`;
else url = `${c.PORNHUB}/video/search?search=${spacer(key)}&o=${sort}&page=${page}`;
console.log(url);
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
return res.json(data);
} catch (err) {
const e = err as Error;
res.status(400).json(maybeError(false, e.message));
}
}

View File

@@ -1,56 +1,15 @@
import { scrapeContent } from "../../scraper/redtube/redtubeGetController"; import { scrapeContent } from "../../scraper/redtube/redtubeGetController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
export async function getRedtube(req: Request, res: Response) { export async function getRedtube({ query }: { query: { id: string } }) {
try { try {
const id = req.query.id as string; const { id } = query;
if (!id) throw Error("Parameter id is required");
if (isNaN(Number(id))) throw Error("Parameter id must be a number");
/**
* @api {get} /redtube/get?id=:id Get Redtube
* @apiName Get redtube
* @apiGroup redtube
* @apiDescription Get a redtube video based on id
*
* @apiParam {String} id Video ID
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/redtube/get?id=42763661
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/redtube/get?id=42763661")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/redtube/get?id=42763661") as resp:
* print(await resp.json())
*/
const url = `${c.REDTUBE}/${id}`; const url = `${c.REDTUBE}/${id}`;
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,54 +1,14 @@
import { scrapeContent } from "../../scraper/redtube/redtubeSearchController"; import { scrapeContent } from "../../scraper/redtube/redtubeSearchController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier"; export async function relatedRedtube({ query }: { query: { id: string } }) {
import { Request, Response } from "express"; try {
const { id } = query;
export async function relatedRedtube(req: Request, res: Response) { const url = `${c.REDTUBE}/${id}`;
try { const data = await scrapeContent(url);
/** return data;
* @api {get} /redtube/get?id=:id Get redtube related videos } catch (err) {
* @apiName Get redtube related videos const e = err as Error;
* @apiGroup redtube throw new Error(e.message);
* @apiDescription Get a related redtube videos based on id }
* }
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/redtube/get?id=41698751
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/redtube/get?id=41698751")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/redtube/get?id=41698751") as resp:
* print(await resp.json())
*/
const id = req.query.id as string;
if (!id) throw Error("Parameter key is required");
if (isNaN(Number(id))) throw Error("Parameter id must be a number");
const url = `${c.REDTUBE}/${id}`;
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
return res.json(data);
} catch (err) {
const e = err as Error;
res.status(400).json(maybeError(false, e.message));
}
}

View File

@@ -1,65 +1,25 @@
import { scrapeContent } from "../../scraper/redtube/redtubeGetController"; import { scrapeContent } from "../../scraper/redtube/redtubeGetController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
import { load } from "cheerio"; import { load } from "cheerio";
import LustPress from "../../LustPress"; import { lust } from "../../LustPress";
const lust = new LustPress(); export async function randomRedtube() {
export async function randomRedtube(req: Request, res: Response) {
try { try {
/**
* @api {get} /redtube/random Get random redtube
* @apiName Get random redtube
* @apiGroup redtube
* @apiDescription Get a random redtube video
*
* @apiParam {String} id Video ID
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/redtube/random
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/redtube/random")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/redtube/random") as resp:
* print(await resp.json())
*/
const resolve = await lust.fetchBody(c.REDTUBE); const resolve = await lust.fetchBody(c.REDTUBE);
const $ = load(resolve); const $ = load(resolve);
const search = $("a.video_link") const search = $("a.video_link")
.map((i, el) => { .map((i, el) => {
return $(el).attr("href"); return $(el).attr("href");
}).get(); })
.get();
const random = Math.floor(Math.random() * search.length); const random = Math.floor(Math.random() * search.length);
const url = c.REDTUBE + search[random]; const url = c.REDTUBE + search[random];
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,58 +1,21 @@
import { scrapeContent } from "../../scraper/redtube/redtubeSearchController"; import { scrapeContent } from "../../scraper/redtube/redtubeSearchController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger"; import { spacer } from "../../utils/modifier";
import { maybeError, spacer } from "../../utils/modifier";
import { Request, Response } from "express"; export async function searchRedtube({
query,
export async function searchRedtube(req: Request, res: Response) { }: {
try { query: { key: string; page?: string };
/** }) {
* @api {get} /redtube/search Search redtube videos try {
* @apiName Search redtube const { key } = query;
* @apiGroup redtube const page = query.page || "1";
* @apiDescription Search redtube videos
* @apiParam {String} key Keyword to search const url = `${c.REDTUBE}/?search=${spacer(key)}&page=${page}`;
* @apiParam {Number} [page=1] Page number const data = await scrapeContent(url);
* return data;
* @apiSuccessExample {json} Success-Response: } catch (err) {
* HTTP/1.1 200 OK const e = err as Error;
* HTTP/1.1 400 Bad Request throw new Error(e.message);
* }
* @apiExample {curl} curl }
* curl -i https://lust.scathach.id/redtube/search?key=milf
* curl -i https://lust.scathach.id/redtube/search?key=milf&page=2
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/redtube/search?key=milf")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/redtube/search?key=milf") as resp:
* print(await resp.json())
*/
const key = req.query.key as string;
const page = req.query.page || 1;
if (!key) throw Error("Parameter key is required");
if (isNaN(Number(page))) throw Error("Parameter page must be a number");
const url = `${c.REDTUBE}/?search=${spacer(key)}&page=${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")
});
return res.json(data);
} catch (err) {
const e = err as Error;
res.status(400).json(maybeError(false, e.message));
}
}

View File

@@ -0,0 +1,69 @@
import { lust } from "../../LustPress";
import { IVideoData, TxxxResponse } from "../../interfaces";
// Generate sharded API URL exactly like TXXX expects
function getApiUrl(videoId: string): string {
const id = Number(videoId);
if (Number.isNaN(id)) throw new Error("Invalid video id");
const million = Math.floor(id / 1_000_000) * 1_000_000;
const thousand = Math.floor(id / 1_000) * 1_000;
return `https://txxx.com/api/json/video/86400/${million}/${thousand}/${id}.json`;
}
export async function getTxxx({ query }: { query: { id: string } }) {
try {
const { id } = query;
const apiUrl = getApiUrl(id);
const buffer = await lust.fetchBody(apiUrl);
const parsed = JSON.parse(buffer.toString("utf-8")) as TxxxResponse;
if (!parsed?.video) {
throw new Error("Invalid API response");
}
const video = parsed.video;
const categories = Object.values(video.categories || {}).map(
(c) => c.title
);
const tags = Object.values(video.tags || {}).map((t) => t.title);
const models = Object.values(video.models || {}).map((m) => m.title);
const videoDir = video.dir || "";
const videoId = video.video_id;
const embed = `https://txxx.com/embed/${videoId}/`;
const response: IVideoData = {
success: true,
data: {
title: video.title || "None",
id: `${videoId}`,
image: video.thumbsrc || video.thumb || "None",
duration: video.duration || "None",
views: video.statistics?.viewed || "0",
rating: video.statistics?.rating || "0.00",
uploaded: video.post_date || "None",
upvoted: String(video.statistics?.likes || "0"),
downvoted: String(video.statistics?.dislikes || "0"),
channel: video.channel?.title || "",
models: models.length > 0 ? models : video.models_suggested || [],
tags: [...categories, ...tags],
},
source: `https://txxx.com/videos/${videoId}/${videoDir}/`,
assets: [embed, video.thumbsrc].filter(Boolean) as string[],
};
return response;
} catch (err) {
const e = err as Error;
throw new Error(e.message);
}
}

View File

@@ -0,0 +1,60 @@
import { lust } from "../../LustPress";
import { ISearchVideoData, TxxxRelatedResponse } from "../../interfaces";
function getRelatedApiUrl(videoId: string, page = 1, count = 50): string {
const id = Number(videoId);
if (Number.isNaN(id)) throw new Error("Invalid video id");
const million = Math.floor(id / 1_000_000) * 1_000_000;
const thousand = Math.floor(id / 1_000) * 1_000;
return (
"https://txxx.com/api/json/videos_related2/" +
`432000/${count}/${million}/${thousand}/${id}.all.${page}.json`
);
}
export async function relatedTxxx({
query,
}: {
query: { id: string; page?: string };
}) {
try {
const { id } = query;
const page = Number(query.page || 1);
if (!id) throw new Error("Parameter id is required");
if (Number.isNaN(page)) throw new Error("Parameter page must be a number");
const apiUrl = getRelatedApiUrl(id, page);
const buffer = await lust.fetchBody(apiUrl);
const rawData = JSON.parse(buffer.toString("utf-8")) as TxxxRelatedResponse;
const videos = Array.isArray(rawData.videos) ? rawData.videos : [];
const data = videos.map((v) => ({
id: String(v.video_id),
title: v.title,
image: v.scr || v.thumb || "",
duration: v.duration || "None",
views: v.video_viewed || "0",
rating: v.rating || "0",
uploader: v.username || v.display_name || "",
link: `https://txxx.com/videos/${v.video_id}/${v.dir}/`,
video: `https://txxx.com/embed/${v.video_id}/`,
}));
return {
success: true,
total_count: String(rawData.total_count || "0"),
pages: rawData.pages || 1,
page,
data,
source: `https://txxx.com/videos/${id}/`,
} as unknown as ISearchVideoData;
} catch (err) {
const e = err as Error;
throw new Error(e.message);
}
}

View File

@@ -0,0 +1,47 @@
import { lust } from "../../LustPress";
import { TxxxSearchResponse as TxxxResponse } from "../../interfaces";
export async function randomTxxx() {
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")) as TxxxResponse;
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}/`,
};
return {
success: true,
data,
source: apiUrl,
};
} catch (err) {
const e = err as Error;
throw new Error(e.message);
}
}

View File

@@ -0,0 +1,52 @@
import { lust } from "../../LustPress";
import { TxxxSearchResponse } from "../../interfaces";
export async function searchTxxx({
query,
}: {
query: { key: string; page?: string };
}) {
try {
const { key } = query;
const page = Number(query.page || 1);
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")) as TxxxSearchResponse;
const videos = Array.isArray(rawData.videos) ? rawData.videos : [];
const data = videos.map((v) => ({
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 {
success: true,
total_count: String(rawData.total_count ?? videos.length),
pages: Number(rawData.pages ?? 1),
page,
data,
};
} catch (err) {
const e = err as Error;
throw new Error(e.message);
}
}

View File

@@ -1,55 +1,15 @@
import { scrapeContent } from "../../scraper/xhamster/xhamsterGetController"; import { scrapeContent } from "../../scraper/xhamster/xhamsterGetController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
export async function getXhamster(req: Request, res: Response) { export async function getXhamster({ query }: { query: { id: string } }) {
try { try {
const id = req.query.id as string; const { id } = query;
if (!id) throw Error("Parameter id is required"); const url = `${c.XHAMSTER}/videos/${id}`;
/**
* @api {get} /xhamster/get?id=:id Get xhamster
* @apiName Get xhamster
* @apiGroup xhamster
* @apiDescription Get a xhamster video based on id
*
* @apiParam {String} id Video ID
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx") as resp:
* print(await resp.json())
*/
const url = `${c.XHAMSTER}/${id}`;
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,55 +1,15 @@
import { scrapeContent } from "../../scraper/xhamster/xhamsterSearchController"; import { scrapeContent } from "../../scraper/xhamster/xhamsterSearchController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
export async function relatedXhamster(req: Request, res: Response) { export async function relatedXhamster({ query }: { query: { id: string } }) {
try { try {
const id = req.query.id as string; const { id } = query;
if (!id) throw Error("Parameter id is required");
/**
* @api {get} /xhamster/get?id=:id Get related xhamster
* @apiName Get related xhamster
* @apiGroup xhamster
* @apiDescription Get a xhamster video based on related id
*
* @apiParam {String} id Video ID
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/xhamster/related?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/xhamster/related?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/xhamster/related?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx") as resp:
* print(await resp.json())
*/
const url = `${c.XHAMSTER}/${id}`; const url = `${c.XHAMSTER}/${id}`;
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,63 +1,31 @@
import { scrapeContent } from "../../scraper/xhamster/xhamsterGetController"; import { scrapeContent } from "../../scraper/xhamster/xhamsterGetController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
import { load } from "cheerio"; import { load } from "cheerio";
import LustPress from "../../LustPress"; import { lust } from "../../LustPress";
const lust = new LustPress(); export async function randomXhamster() {
export async function randomXhamster(req: Request, res: Response) {
try { try {
/**
* @api {get} /xhamster/random Get random xhamster
* @apiName Get random xhamster
* @apiGroup xhamster
* @apiDescription Get a random xhamster video
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/xhamster/random
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/xhamster/random")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/xhamster/random") as resp:
* print(await resp.json())
*/
const resolve = await lust.fetchBody(`${c.XHAMSTER}/newest`); const resolve = await lust.fetchBody(`${c.XHAMSTER}/newest`);
const $ = load(resolve); const $ = load(resolve);
const search = $("a.root-9d8b4.video-thumb-info__name.role-pop.with-dropdown")
.map((i, el) => $(el).attr("href"))
.get();
const search_ = search.map((el) => el.replace(c.XHAMSTER, "")); const videoLinks = $("div.thumb-list__item[data-video-id]")
const random = Math.floor(Math.random() * search_.length); .map((_, el) => {
const url = c.XHAMSTER + search_[random]; const href = $(el).find("a[data-role='thumb-link']").attr("href");
const data = await scrapeContent(url); return href && href.includes("/videos/") ? href : null;
logger.info({ })
path: req.path, .get()
query: req.query, .filter(Boolean);
method: req.method,
ip: req.ip, // Select a random video URL from the list
useragent: req.get("User-Agent") const randomIndex = Math.floor(Math.random() * videoLinks.length);
}); const randomUrl = videoLinks[randomIndex];
return res.json(data);
const data = await scrapeContent(randomUrl);
return data;
} catch (err) { } catch (err) {
const e = err as Error; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,58 +1,21 @@
import { scrapeContent } from "../../scraper/xhamster/xhamsterSearchController"; import { scrapeContent } from "../../scraper/xhamster/xhamsterSearchController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger"; import { spacer } from "../../utils/modifier";
import { maybeError, spacer } from "../../utils/modifier";
import { Request, Response } from "express"; export async function searchXhamster({
query,
export async function searchXhamster(req: Request, res: Response) { }: {
try { query: { key: string; page?: string };
/** }) {
* @api {get} /xhamster/search Search xhamster videos try {
* @apiName Search xhamster const { key } = query;
* @apiGroup xhamster const page = query.page || "1";
* @apiDescription Search xhamster videos
* @apiParam {String} key Keyword to search const url = `${c.XHAMSTER}/search/${spacer(key)}?page=${page}`;
* @apiParam {Number} [page=1] Page number const data = await scrapeContent(url);
* return data;
* @apiSuccessExample {json} Success-Response: } catch (err) {
* HTTP/1.1 200 OK const e = err as Error;
* HTTP/1.1 400 Bad Request throw new Error(e.message);
* }
* @apiExample {curl} curl }
* curl -i https://lust.scathach.id/xhamster/search?key=milf
* curl -i https://lust.scathach.id/xhamster/search?key=milf&page=2
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/xhamster/search?key=milf")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/xhamster/search?key=milf") as resp:
* print(await resp.json())
*/
const key = req.query.key as string;
const page = req.query.page || 1;
if (!key) throw Error("Parameter key is required");
if (isNaN(Number(page))) throw Error("Parameter page must be a number");
const url = `${c.XHAMSTER}/search/${spacer(key)}?page=${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")
});
return res.json(data);
} catch (err) {
const e = err as Error;
res.status(400).json(maybeError(false, e.message));
}
}

View File

@@ -1,55 +1,15 @@
import { scrapeContent } from "../../scraper/xnxx/xnxxGetController"; import { scrapeContent } from "../../scraper/xnxx/xnxxGetController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
export async function getXnxx(req: Request, res: Response) { export async function getXnxx({ query }: { query: { id: string } }) {
try { try {
const id = req.query.id as string; const { id } = query;
if (!id) throw Error("Parameter id is required");
/**
* @api {get} /xnxx/get?id=:id Get xnxx
* @apiName Get xnxx
* @apiGroup xnxx
* @apiDescription Get a xnxx video based on id
*
* @apiParam {String} id Video ID
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/xnxx/get?id=video-17vah71a/makima_y_denji
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/xnxx/get?id=video-17vah71a/makima_y_denji")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/xnxx/get?id=video-17vah71a/makima_y_denji") as resp:
* print(await resp.json())
*/
const url = `${c.XNXX}/${id}`; const url = `${c.XNXX}/${id}`;
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,55 +1,15 @@
import { scrapeContent } from "../../scraper/xnxx/xnxxGetRelatedController"; import { scrapeContent } from "../../scraper/xnxx/xnxxGetRelatedController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
export async function relatedXnxx(req: Request, res: Response) { export async function relatedXnxx({ query }: { query: { id: string } }) {
try { try {
const id = req.query.id as string; const { id } = query;
if (!id) throw Error("Parameter id is required");
/**
* @api {get} /xnxx/get?id=:id Get related xnxx
* @apiName Get related xnxx
* @apiGroup xnxx
* @apiDescription Get a xnxx video based on related id
*
* @apiParam {String} id Video ID
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/xnxx/related?id=video-17vah71a/makima_y_denji
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/xnxx/related?id=video-17vah71a/makima_y_denji")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/xnxx/related?id=video-17vah71a/makima_y_denji") as resp:
* print(await resp.json())
*/
const url = `${c.XNXX}/${id}`; const url = `${c.XNXX}/${id}`;
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,63 +1,27 @@
import { scrapeContent } from "../../scraper/xnxx/xnxxGetController"; import { scrapeContent } from "../../scraper/xnxx/xnxxGetController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger"; import { lust } from "../../LustPress";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
import { load } from "cheerio"; import { load } from "cheerio";
import LustPress from "../../LustPress";
const lust = new LustPress(); export async function randomXnxx() {
export async function randomXnxx(req: Request, res: Response) {
try { try {
const resolve = await lust.fetchBody(
"https://www.xnxx.com/search/random/random"
/** );
* @api {get} /xnxx/random Get random xnxx
* @apiName Get random xnxx
* @apiGroup xnxx
* @apiDescription Get a random xnxx video
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/xnxx/random
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/xnxx/random")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/xnxx/random") as resp:
* print(await resp.json())
*/
const resolve = await lust.fetchBody("https://www.xnxx.com/search/random/random");
const $ = load(resolve); const $ = load(resolve);
const search = $("div.mozaique > div") const search = $("div.mozaique > div")
.map((i, el) => { .map((i, el) => {
return $(el).find("a").attr("href"); return $(el).find("a").attr("href");
}).get(); })
.get();
const random = Math.floor(Math.random() * search.length); const random = Math.floor(Math.random() * search.length);
const url = c.XNXX + search[random]; const url = c.XNXX + search[random];
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,58 +1,20 @@
import { scrapeContent } from "../../scraper/xnxx/xnxxSearchController"; import { scrapeContent } from "../../scraper/xnxx/xnxxSearchController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger"; import { spacer } from "../../utils/modifier";
import { maybeError, spacer } from "../../utils/modifier";
import { Request, Response } from "express"; export async function searchXnxx({
query,
export async function searchXnxx(req: Request, res: Response) { }: {
try { query: { key: string; page?: string };
/** }) {
* @api {get} /xnxx/search Search xnxx videos try {
* @apiName Search xnxx const { key } = query;
* @apiGroup xnxx const page = query.page || "0";
* @apiDescription Search xnxx videos const url = `${c.XNXX}/search/${spacer(key)}/${page}`;
* @apiParam {String} key Keyword to search const data = await scrapeContent(url);
* @apiParam {Number} [page=0] Page number return data;
* } catch (err) {
* @apiSuccessExample {json} Success-Response: const e = err as Error;
* HTTP/1.1 200 OK throw new Error(e.message);
* HTTP/1.1 400 Bad Request }
* }
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/xnxx/search?key=milf
* curl -i https://lust.scathach.id/xnxx/search?key=milf&page=2
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/xnxx/search?key=milf")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/xnxx/search?key=milf") as resp:
* print(await resp.json())
*/
const key = req.query.key as string;
const page = req.query.page || 0;
if (!key) throw Error("Parameter key is required");
if (isNaN(Number(page))) throw Error("Parameter page must be a number");
const url = `${c.XNXX}/search/${spacer(key)}/${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")
});
return res.json(data);
} catch (err) {
const e = err as Error;
res.status(400).json(maybeError(false, e.message));
}
}

View File

@@ -1,55 +1,15 @@
import { scrapeContent } from "../../scraper/xvideos/xvideosGetController"; import { scrapeContent } from "../../scraper/xvideos/xvideosGetController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
export async function getXvideos(req: Request, res: Response) { export async function getXvideos({ query }: { query: { id: string } }) {
try { try {
const id = req.query.id as string; const { id } = query;
if (!id) throw Error("Parameter id is required");
/**
* @api {get} /xvideos/get?id=:id Get xvideos
* @apiName Get xvideos
* @apiGroup xvideos
* @apiDescription Get a xvideos video based on id
*
* @apiParam {String} id Video ID
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/xvideos/get?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/xvideos/get?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/xvideos/get?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_") as resp:
* print(await resp.json())
*/
const url = `${c.XVIDEOS}/${id}`; const url = `${c.XVIDEOS}/${id}`;
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,55 +1,15 @@
import { scrapeContent } from "../../scraper/xvideos/xvideosGetRelatedController"; import { scrapeContent } from "../../scraper/xvideos/xvideosGetRelatedController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
export async function relatedXvideos(req: Request, res: Response) { export async function relatedXvideos({ query }: { query: { id: string } }) {
try { try {
const id = req.query.id as string; const { id } = query;
if (!id) throw Error("Parameter id is required");
/**
* @api {get} /xvideos/get?id=:id Get related xvideos
* @apiName Get related xvideos
* @apiGroup xvideos
* @apiDescription Get a xvideos video based on related id
*
* @apiParam {String} id Video ID
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/xvideos/related?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/xvideos/related?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/xvideos/related?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_") as resp:
* print(await resp.json())
*/
const url = `${c.XVIDEOS}/${id}`; const url = `${c.XVIDEOS}/${id}`;
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,41 +1,10 @@
import { load } from "cheerio";
import { scrapeContent } from "../../scraper/xvideos/xvideosGetController"; import { scrapeContent } from "../../scraper/xvideos/xvideosGetController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger"; import { lust } from "../../LustPress";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
import { load } from "cheerio";
import LustPress from "../../LustPress";
const lust = new LustPress(); export async function randomXvideos() {
export async function randomXvideos(req: Request, res: Response) {
try { try {
/**
* @api {get} /xvideos/random Get random xvideos
* @apiName Get random xvideos
* @apiGroup xvideos
* @apiDescription Get a random xvideos video
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/xvideos/random
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/xvideos/random")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/xvideos/random") as resp:
* print(await resp.json())
*/
const resolve = await lust.fetchBody(c.XVIDEOS); const resolve = await lust.fetchBody(c.XVIDEOS);
const $ = load(resolve); const $ = load(resolve);
const search = $("div.thumb-under") const search = $("div.thumb-under")
@@ -45,19 +14,13 @@ export async function randomXvideos(req: Request, res: Response) {
const filtered = search.filter((el) => el.includes("/video")); const filtered = search.filter((el) => el.includes("/video"));
const filtered_ = filtered.filter((el) => !el.includes("THUMBNUM")); const filtered_ = filtered.filter((el) => !el.includes("THUMBNUM"));
const random = Math.floor(Math.random() * filtered_.length); const random = Math.floor(Math.random() * filtered_.length);
const url = c.XVIDEOS + filtered[random]; const url = c.XVIDEOS + filtered_[random];
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,58 +1,21 @@
import { scrapeContent } from "../../scraper/xvideos/xvideosSearchController"; import { scrapeContent } from "../../scraper/xvideos/xvideosSearchController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger"; import { spacer } from "../../utils/modifier";
import { maybeError, spacer } from "../../utils/modifier";
import { Request, Response } from "express"; export async function searchXvideos({
query,
export async function searchXvideos(req: Request, res: Response) { }: {
try { query: { key: string; page?: string };
/** }) {
* @api {get} /xvideos/search Search xvideos videos try {
* @apiName Search xvideos const { key } = query;
* @apiGroup xvideos const page = query.page || "0";
* @apiDescription Search xvideos videos
* @apiParam {String} key Keyword to search const url = `${c.XVIDEOS}/?k=${spacer(key)}&p=${page}`;
* @apiParam {Number} [page=0] Page number const data = await scrapeContent(url);
* return data;
* @apiSuccessExample {json} Success-Response: } catch (err) {
* HTTP/1.1 200 OK const e = err as Error;
* HTTP/1.1 400 Bad Request throw new Error(e.message);
* }
* @apiExample {curl} curl }
* curl -i https://lust.scathach.id/xvideos/search?key=milf
* curl -i https://lust.scathach.id/xvideos/search?key=milf&page=2
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/xvideos/search?key=milf")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/xvideos/search?key=milf") as resp:
* print(await resp.json())
*/
const key = req.query.key as string;
const page = req.query.page || 0;
if (!key) throw Error("Parameter key is required");
if (isNaN(Number(page))) throw Error("Parameter page must be a number");
const url = `${c.XVIDEOS}/?k=${spacer(key)}&p=${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")
});
return res.json(data);
} catch (err) {
const e = err as Error;
res.status(400).json(maybeError(false, e.message));
}
}

View File

@@ -1,55 +1,15 @@
import { scrapeContent } from "../../scraper/youporn/youpornGetController"; import { scrapeContent } from "../../scraper/youporn/youpornGetController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
export async function getYouporn(req: Request, res: Response) { export async function getYouporn({ query }: { query: { id: string } }) {
try { try {
const id = req.query.id as string; const { id } = query;
if (!id) throw Error("Parameter id is required");
/**
* @api {get} /youporn/get?id=:id Get youporn
* @apiName Get youporn
* @apiGroup youporn
* @apiDescription Get a youporn video based on id
*
* @apiParam {String} id Video ID
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/youporn/get?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/youporn/get?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/youporn/get?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps") as resp:
* print(await resp.json())
*/
const url = `${c.YOUPORN}/watch/${id}`; const url = `${c.YOUPORN}/watch/${id}`;
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,55 +1,15 @@
import { scrapeContent } from "../../scraper/youporn/youpornSearchController"; import { scrapeContent } from "../../scraper/youporn/youpornSearchController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
export async function relatedYouporn(req: Request, res: Response) { export async function relatedYouporn({ query }: { query: { id: string } }) {
try { try {
const id = req.query.id as string; const { id } = query;
if (!id) throw Error("Parameter id is required");
/**
* @api {get} /youporn/get?id=:id Get related youporn
* @apiName Get related youporn
* @apiGroup youporn
* @apiDescription Get a youporn video based on related id
*
* @apiParam {String} id Video ID
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/youporn/related?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/youporn/related?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/youporn/related?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps") as resp:
* print(await resp.json())
*/
const url = `${c.YOUPORN}/watch/${id}`; const url = `${c.YOUPORN}/watch/${id}`;
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,62 +1,23 @@
import { scrapeContent } from "../../scraper/youporn/youpornGetController"; import { scrapeContent } from "../../scraper/youporn/youpornGetController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
import { load } from "cheerio"; import { load } from "cheerio";
import LustPress from "../../LustPress"; import { lust } from "../../LustPress";
const lust = new LustPress(); export async function randomYouporn() {
export async function randomYouporn(req: Request, res: Response) {
try { try {
/**
* @api {get} /youporn/random Get random youporn
* @apiName Get random youporn
* @apiGroup youporn
* @apiDescription Get a random youporn video
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/youporn/random
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/youporn/random")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/youporn/random") as resp:
* print(await resp.json())
*/
const resolve = await lust.fetchBody(`${c.YOUPORN}`); const resolve = await lust.fetchBody(`${c.YOUPORN}`);
const $ = load(resolve); const $ = load(resolve);
const search = $("a[href^='/watch/']") const search = $("a[href^='/watch/']")
.map((i, el) => { .map((i, el) => {
return $(el).attr("href"); return $(el).attr("href");
}).get(); })
.get();
const random = Math.floor(Math.random() * search.length); const random = Math.floor(Math.random() * search.length);
const url = c.YOUPORN + search[random]; const url = c.YOUPORN + search[random];
const data = await scrapeContent(url); const data = await scrapeContent(url);
logger.info({ return data;
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; const e = err as Error;
res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,58 +1,21 @@
import { scrapeContent } from "../../scraper/youporn/youpornSearchController"; import { scrapeContent } from "../../scraper/youporn/youpornSearchController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger"; import { spacer } from "../../utils/modifier";
import { maybeError, spacer } from "../../utils/modifier";
import { Request, Response } from "express"; export async function searchYouporn({
query,
export async function searchYouporn(req: Request, res: Response) { }: {
try { query: { key: string; page?: string };
/** }) {
* @api {get} /youporn/search Search youporn videos try {
* @apiName Search youporn const { key } = query;
* @apiGroup youporn const page = query.page || "1";
* @apiDescription Search youporn videos
* @apiParam {String} key Keyword to search const url = `${c.YOUPORN}/search/?query=${spacer(key)}&page=${page}`;
* @apiParam {Number} [page=1] Page number const data = await scrapeContent(url);
* return data;
* @apiSuccessExample {json} Success-Response: } catch (err) {
* HTTP/1.1 200 OK const e = err as Error;
* HTTP/1.1 400 Bad Request throw new Error(e.message);
* }
* @apiExample {curl} curl }
* curl -i https://lust.scathach.id/youporn/search?key=milf
* curl -i https://lust.scathach.id/youporn/search?key=milf&page=2
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/youporn/search?key=milf")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/youporn/search?key=milf") as resp:
* print(await resp.json())
*/
const key = req.query.key as string;
const page = req.query.page || 1;
if (!key) throw Error("Parameter key is required");
if (isNaN(Number(page))) throw Error("Parameter page must be a number");
const url = `${c.YOUPORN}/search/?query=${spacer(key)}&page=${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")
});
return res.json(data);
} catch (err) {
const e = err as Error;
res.status(400).json(maybeError(false, e.message));
}
}

View File

@@ -1,52 +1,56 @@
import "dotenv/config"; import { Elysia } from "elysia";
import LustPress from "./LustPress"; import { cors } from "@elysiajs/cors";
import express from "express"; import { swagger } from "@elysiajs/swagger";
import { Request, Response, NextFunction } from "express"; import { lust } from "./LustPress";
import scrapeRoutes from "./router/endpoint"; import { scrapeRoutes } from "./router/endpoint";
import { slow, limiter } from "./utils/limit-options"; import pkg from "../package.json";
import { logger } from "./utils/logger";
import * as pkg from "../package.json"; const app = new Elysia()
.use(cors())
const lust = new LustPress(); .use(
const app = express(); swagger({
path: "/docs",
documentation: {
app.get("/", slow, limiter, async (req, res) => { info: {
res.send({ title: "Lustpress API",
success: true, version: pkg.version,
playground: "https://sinkaroid.github.io/lustpress", description: pkg.description,
endpoint: "https://github.com/sinkaroid/lustpress/blob/master/README.md#routing", },
date: new Date().toLocaleString(), },
rss: lust.currentProccess().rss, })
heap: lust.currentProccess().heap, )
server: await lust.getServer(), .get("/", async () => ({
version: `${pkg.version}`, success: true,
}); playground: "https://sinkaroid.github.io/lustpress",
logger.info({ endpoint:
path: req.path, "https://github.com/sinkaroid/lustpress/blob/master/README.md#routing",
method: req.method, date: new Date().toLocaleString(),
ip: req.ip, rss: lust.currentProccess().rss,
useragent: req.get("User-Agent") heap: lust.currentProccess().heap,
}); server: await lust.getServer(),
}); version: pkg.version,
}))
app.use(scrapeRoutes()); .use(scrapeRoutes)
app.use((req: Request, res: Response, next: NextFunction) => { .onError(({ code, error, set }) => {
res.status(404); console.log("Error occurred:", error);
next(Error(`The page not found in path ${req.url} and method ${req.method}`)); if (code === "NOT_FOUND") {
logger.error({ set.status = 404;
path: req.url, return {
method: req.method, success: false,
ip: req.ip, message: (error as Error).message || "Not Found",
useragent: req.get("User-Agent") };
}); }
}); set.status = 500;
return {
app.use((error: any, res: Response) => { success: false,
res.status(500).json({ message: (error as Error).message || "Internal Server Error",
message: error.message, stack: (error as Error).stack,
stack: error.stack };
}); })
}); .listen(process.env.PORT || 3000);
app.listen(process.env.PORT || 3000, () => console.log(`${pkg.name} is running on port ${process.env.PORT || 3000}`)); console.log(
`Lustpress is running at ${app.server?.hostname}:${app.server?.port}`
);
export type App = typeof app;

View File

@@ -1,29 +1,195 @@
// Global response types
export interface IVideoData { export interface IVideoData {
success: boolean; success: boolean;
data: { data: {
title: string; title: string;
id: string; id: string;
image: string; image?: string;
duration: string; duration: string;
views: string; views: string;
rating: string; rating?: string;
uploaded: string; uploaded: string;
upvoted: string | null; upvoted: string | null;
downvoted: string | null; downvoted: string | null;
models: string[]; channel?: string;
tags: string[]; models: string[];
}; tags: string[];
source: string; };
assets: string[]; source: string;
assets: string[];
} }
export interface ISearchVideoData { export interface ISearchVideoData {
success: boolean; success: boolean;
data: string[]; data: string[];
source: string; source: string;
}
export interface ISearchItem {
link: string;
id: string;
title?: string;
image?: string;
duration?: string;
rating?: string;
views?: string;
uploader?: string;
video?: string;
} }
export interface MaybeError { export interface MaybeError {
message: string; message: string;
} }
// Pornhub
export interface PornhubSearchItem {
link: string;
id: string;
title: string;
image: string;
duration: string;
views: string;
video: string;
}
// Xvideos
export interface XvideosSearchItem {
link: string;
id: string;
image: string;
title: string;
duration: string;
rating: string | null;
video: string;
}
export interface XvideosRelatedRaw {
u: string;
t: string;
i: string;
d: string;
n: string;
r: string;
id: string | number;
}
// Xnxx
export interface XnxxRelatedRaw {
u: string;
t: string;
i: string;
d: string;
n: string;
r: string;
id: string | number;
}
// Xhamster
export interface XhamsterSearchItem {
link: string;
id: string;
title: string;
image: string;
duration: string;
views: string;
video: string;
}
export interface XhamsterTag {
isTag?: boolean;
isPornstar?: boolean;
name: string;
}
export interface XhamsterInitials {
ratingComponent?: {
ratingModel?: {
value?: number;
likes?: number;
dislikes?: number;
};
};
videoTagsComponent?: {
tags?: XhamsterTag[];
};
}
// Redtube
export interface RedTubeSearchItem {
link: string;
id: string;
title: string;
image: string;
duration: string;
views: string;
video: string;
}
// Txxx
export interface TxxxCategory {
title: string;
}
export interface TxxxVideo {
title?: string;
video_id: string | number;
thumbsrc?: string;
thumb?: string;
duration?: string;
statistics?: {
viewed?: string;
rating?: string;
likes?: number;
dislikes?: number;
};
post_date?: string;
channel?: { title?: string };
categories?: Record<string, TxxxCategory>;
tags?: Record<string, TxxxCategory>;
models?: Record<string, TxxxCategory>;
models_suggested?: string[];
dir?: string;
}
export interface TxxxResponse {
video?: TxxxVideo;
}
export interface TxxxSearchVideo {
video_id: string | number;
title: string;
dir?: string;
duration?: string;
video_viewed?: string;
rating?: string;
post_date?: string;
likes?: number;
dislikes?: number;
scr?: string;
categories?: string;
}
export interface TxxxSearchResponse {
videos?: TxxxSearchVideo[];
total_count?: string | number;
pages?: number;
}
export interface TxxxRelatedVideo {
video_id: string | number;
title: string;
scr?: string;
thumb?: string;
duration?: string;
video_viewed?: string;
rating?: string;
username?: string;
display_name?: string;
dir?: string;
}
export interface TxxxRelatedResponse {
videos?: TxxxRelatedVideo[];
total_count?: string | number;
pages?: number;
}

View File

@@ -1,72 +1,207 @@
import cors from "cors"; import { Elysia, t } from "elysia";
import { Router } from "express";
import { slow, limiter } from "../utils/limit-options"; // EPorner
import { getEporner } from "../controller/eporner/epornerGet";
// PornHub import { searchEporner } from "../controller/eporner/epornerSearch";
import { getPornhub } from "../controller/pornhub/pornhubGet"; import { relatedEporner } from "../controller/eporner/epornerGetRelated";
import { searchPornhub } from "../controller/pornhub/pornhubSearch"; import { randomEporner } from "../controller/eporner/epornerRandom";
import { randomPornhub } from "../controller/pornhub/pornhubRandom";
import { relatedPornhub } from "../controller/pornhub/pornhubGetRelated"; // TXXX
import { getTxxx } from "../controller/txxx/txxxGet";
// XNXX import { searchTxxx } from "../controller/txxx/txxxSearch";
import { getXnxx } from "../controller/xnxx/xnxxGet"; import { relatedTxxx } from "../controller/txxx/txxxGetRelated";
import { searchXnxx } from "../controller/xnxx/xnxxSearch"; import { randomTxxx } from "../controller/txxx/txxxRandom";
import { relatedXnxx } from "../controller/xnxx/xnxxGetRelated";
import { randomXnxx } from "../controller/xnxx/xnxxRandom"; // PornHub
import { getPornhub } from "../controller/pornhub/pornhubGet";
// RedTube import { searchPornhub } from "../controller/pornhub/pornhubSearch";
import { getRedtube } from "../controller/redtube/redtubeGet"; import { randomPornhub } from "../controller/pornhub/pornhubRandom";
import { searchRedtube } from "../controller/redtube/redtubeSearch"; import { relatedPornhub } from "../controller/pornhub/pornhubGetRelated";
import { relatedRedtube } from "../controller/redtube/redtubeGetRelated";
import { randomRedtube } from "../controller/redtube/redtubeRandom"; // XNXX
import { getXnxx } from "../controller/xnxx/xnxxGet";
// Xvideos import { searchXnxx } from "../controller/xnxx/xnxxSearch";
import { getXvideos } from "../controller/xvideos/xvideosGet"; import { relatedXnxx } from "../controller/xnxx/xnxxGetRelated";
import { searchXvideos } from "../controller/xvideos/xvideosSearch"; import { randomXnxx } from "../controller/xnxx/xnxxRandom";
import { randomXvideos } from "../controller/xvideos/xvideosRandom";
import { relatedXvideos } from "../controller/xvideos/xvideosGetRelated"; // RedTube
import { getRedtube } from "../controller/redtube/redtubeGet";
// Xhamster import { searchRedtube } from "../controller/redtube/redtubeSearch";
import { getXhamster } from "../controller/xhamster/xhamsterGet"; import { relatedRedtube } from "../controller/redtube/redtubeGetRelated";
import { searchXhamster } from "../controller/xhamster/xhamsterSearch"; import { randomRedtube } from "../controller/redtube/redtubeRandom";
import { randomXhamster } from "../controller/xhamster/xhamsterRandom";
import { relatedXhamster } from "../controller/xhamster/xhamsterGetRelated"; // Xvideos
import { getXvideos } from "../controller/xvideos/xvideosGet";
// YouPorn import { searchXvideos } from "../controller/xvideos/xvideosSearch";
import { getYouporn } from "../controller/youporn/youpornGet"; import { randomXvideos } from "../controller/xvideos/xvideosRandom";
import { searchYouporn } from "../controller/youporn/youpornSearch"; import { relatedXvideos } from "../controller/xvideos/xvideosGetRelated";
import { relatedYouporn } from "../controller/youporn/youpornGetRelated";
import { randomYouporn } from "../controller/youporn/youpornRandom"; // Xhamster
import { getXhamster } from "../controller/xhamster/xhamsterGet";
function scrapeRoutes() { import { searchXhamster } from "../controller/xhamster/xhamsterSearch";
const router = Router(); import { randomXhamster } from "../controller/xhamster/xhamsterRandom";
import { relatedXhamster } from "../controller/xhamster/xhamsterGetRelated";
router.get("/pornhub/get", cors(), slow, limiter, getPornhub);
router.get("/pornhub/search", cors(), slow, limiter, searchPornhub); // YouPorn
router.get("/pornhub/random", cors(), slow, limiter, randomPornhub); import { getYouporn } from "../controller/youporn/youpornGet";
router.get("/pornhub/related", cors(), slow, limiter, relatedPornhub); import { searchYouporn } from "../controller/youporn/youpornSearch";
router.get("/xnxx/get", cors(), slow, limiter, getXnxx); import { relatedYouporn } from "../controller/youporn/youpornGetRelated";
router.get("/xnxx/search", cors(), slow, limiter, searchXnxx); import { randomYouporn } from "../controller/youporn/youpornRandom";
router.get("/xnxx/related", cors(), slow, limiter, relatedXnxx);
router.get("/xnxx/random", cors(), slow, limiter, randomXnxx); const queryId = {
router.get("/redtube/get", cors(), slow, limiter, getRedtube); query: t.Object({ id: t.String() }),
router.get("/redtube/search", cors(), slow, limiter, searchRedtube); };
router.get("/redtube/related", cors(), slow, limiter, relatedRedtube);
router.get("/redtube/random", cors(), slow, limiter, randomRedtube); const querySearch = {
router.get("/xvideos/get", cors(), slow, limiter, getXvideos); query: t.Object({
router.get("/xvideos/search", cors(), slow, limiter, searchXvideos); key: t.String(),
router.get("/xvideos/random", cors(), slow, limiter, randomXvideos); page: t.Optional(t.String()),
router.get("/xvideos/related", cors(), slow, limiter, relatedXvideos); }),
router.get("/xhamster/get", cors(), slow, limiter, getXhamster); };
router.get("/xhamster/search", cors(), slow, limiter, searchXhamster);
router.get("/xhamster/random", cors(), slow, limiter, randomXhamster); export const scrapeRoutes = (app: Elysia) =>
router.get("/xhamster/related", cors(), slow, limiter, relatedXhamster); app
router.get("/youporn/get", cors(), slow, limiter, getYouporn); .group("/pornhub", (app) =>
router.get("/youporn/search", cors(), slow, limiter, searchYouporn); app
router.get("/youporn/related", cors(), slow, limiter, relatedYouporn); .get("/get", getPornhub, {
router.get("/youporn/random", cors(), slow, limiter, randomYouporn); ...queryId,
detail: { summary: "Get Pornhub video data", tags: ["Pornhub"] },
return router; })
} .get("/search", searchPornhub, {
...querySearch,
export default scrapeRoutes; detail: { summary: "Search Pornhub videos", tags: ["Pornhub"] },
})
.get("/random", randomPornhub, {
detail: { summary: "Get random Pornhub videos", tags: ["Pornhub"] },
})
.get("/related", relatedPornhub, {
...queryId,
detail: { summary: "Get related Pornhub videos", tags: ["Pornhub"] },
})
)
.group("/xnxx", (app) =>
app
.get("/get", getXnxx, {
...queryId,
detail: { summary: "Get XNXX video data", tags: ["XNXX"] },
})
.get("/search", searchXnxx, {
...querySearch,
detail: { summary: "Search XNXX videos", tags: ["XNXX"] },
})
.get("/related", relatedXnxx, {
...queryId,
detail: { summary: "Get related XNXX videos", tags: ["XNXX"] },
})
.get("/random", randomXnxx, {
detail: { summary: "Get random XNXX videos", tags: ["XNXX"] },
})
)
.group("/redtube", (app) =>
app
.get("/get", getRedtube, {
...queryId,
detail: { summary: "Get RedTube video data", tags: ["RedTube"] },
})
.get("/search", searchRedtube, {
...querySearch,
detail: { summary: "Search RedTube videos", tags: ["RedTube"] },
})
.get("/related", relatedRedtube, {
...queryId,
detail: { summary: "Get related RedTube videos", tags: ["RedTube"] },
})
.get("/random", randomRedtube, {
detail: { summary: "Get random RedTube videos", tags: ["RedTube"] },
})
)
.group("/xvideos", (app) =>
app
.get("/get", getXvideos, {
...queryId,
detail: { summary: "Get Xvideos video data", tags: ["Xvideos"] },
})
.get("/search", searchXvideos, {
...querySearch,
detail: { summary: "Search Xvideos videos", tags: ["Xvideos"] },
})
.get("/random", randomXvideos, {
detail: { summary: "Get random Xvideos videos", tags: ["Xvideos"] },
})
.get("/related", relatedXvideos, {
...queryId,
detail: { summary: "Get related Xvideos videos", tags: ["Xvideos"] },
})
)
.group("/xhamster", (app) =>
app
.get("/get", getXhamster, {
...queryId,
detail: { summary: "Get Xhamster video data", tags: ["Xhamster"] },
})
.get("/search", searchXhamster, {
...querySearch,
detail: { summary: "Search Xhamster videos", tags: ["Xhamster"] },
})
.get("/random", randomXhamster, {
detail: { summary: "Get random Xhamster videos", tags: ["Xhamster"] },
})
.get("/related", relatedXhamster, {
...queryId,
detail: { summary: "Get related Xhamster videos", tags: ["Xhamster"] },
})
)
.group("/youporn", (app) =>
app
.get("/get", getYouporn, {
...queryId,
detail: { summary: "Get YouPorn video data", tags: ["YouPorn"] },
})
.get("/search", searchYouporn, {
...querySearch,
detail: { summary: "Search YouPorn videos", tags: ["YouPorn"] },
})
.get("/related", relatedYouporn, {
...queryId,
detail: { summary: "Get related YouPorn videos", tags: ["YouPorn"] },
})
.get("/random", randomYouporn, {
detail: { summary: "Get random YouPorn videos", tags: ["YouPorn"] },
})
)
.group("/eporner", (app) =>
app
.get("/get", getEporner, {
...queryId,
detail: { summary: "Get EPorner video data", tags: ["EPorner"] },
})
.get("/search", searchEporner, {
...querySearch,
detail: { summary: "Search EPorner videos", tags: ["EPorner"] },
})
.get("/related", relatedEporner, {
...queryId,
detail: { summary: "Get related EPorner videos", tags: ["EPorner"] },
})
.get("/random", randomEporner, {
detail: { summary: "Get random EPorner videos", tags: ["EPorner"] },
})
)
.group("/txxx", (app) =>
app
.get("/get", getTxxx, {
...queryId,
detail: { summary: "Get TXXX video data", tags: ["TXXX"] },
})
.get("/search", searchTxxx, {
...querySearch,
detail: { summary: "Search TXXX videos", tags: ["TXXX"] },
})
.get("/related", relatedTxxx, {
...queryId,
detail: { summary: "Get related TXXX videos", tags: ["TXXX"] },
})
.get("/random", randomTxxx, {
detail: { summary: "Get random TXXX videos", tags: ["TXXX"] },
})
);

View File

@@ -0,0 +1,95 @@
import { load } from "cheerio";
import { lust } from "../../LustPress";
import { IVideoData } from "../../interfaces";
function calculateRatingFromStrings(
upVote: string,
downVote: string
): number {
const up = parseInt(upVote.replace(/,/g, ""), 10) || 0;
const down = parseInt(downVote.replace(/,/g, ""), 10) || 0;
const total = up + down;
if (total === 0) return 0;
return up / total;
}
export async function scrapeContent(url: string) {
try {
const resolve = await lust.fetchBody(url);
const $ = load(resolve);
class EPorner {
link: string;
id: string;
title: string;
image: string;
duration: string;
views: string;
rating: string;
publish: string;
upVote: string;
downVote: string;
video: string;
tags: string[];
models: string[];
constructor() {
// https://www.eporner.com/video-ibvqvezXzcs/ivy-seduces-her-dad-s-friend/
this.link = $("link[rel='canonical']").attr("href") || "None";
this.id = this.link.split("/")[3] + "/" + this.link.split("/")[4] || "None";
this.title = $("meta[property='og:title']").attr("content") || "None";
this.image = $("meta[property='og:image']").attr("content") || "None";
this.duration = $("meta[property='og:duration']").attr("content") || "0";
this.views = $("#cinemaviews1").text().trim() || "0";
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 jsonLd = JSON.parse(jsonLdText);
// eslint-disable-next-line no-unused-vars
const isoDuration = jsonLd.duration; // "PT00H8M12S"
this.publish = jsonLd.uploadDate || "None";
const ratingValue = calculateRatingFromStrings(this.upVote, this.downVote);
this.rating = (ratingValue * 100).toFixed(2);
const videoPart = this.link.split("/")[3] || "None";
const code = videoPart.replace("video-", "");
this.video = `https://www.eporner.com/embed/${code}/`;
this.tags = $("li.vit-category a")
.map((_, el) => $(el).text().trim()).get();
this.models = $("li.vit-pornstar a")
.map((_, el) => $(el).text().trim()).get();
}
}
const ep = new EPorner();
const data: IVideoData = {
success: true,
data: {
title: lust.removeHtmlTagWithoutSpace(ep.title),
id: ep.id,
image: ep.image,
duration: lust.secondToMinute(Number(ep.duration)),
views: ep.views,
rating: ep.rating,
uploaded: ep.publish,
upvoted: ep.upVote,
downvoted: ep.downVote,
models: ep.models,
tags: ep.tags
},
source: ep.link,
assets: [ep.video, ep.image]
};
return data;
} catch (err) {
const e = err as Error;
throw Error(e.message);
}
}

View File

@@ -0,0 +1,44 @@
import { load } from "cheerio";
import { lust } from "../../LustPress";
import { ISearchVideoData } from "../../interfaces";
export async function scrapeContent(url: string) {
const html = await lust.fetchBody(url);
const $ = load(html);
const data = $("#relateddiv .mb")
.map((i, el) => {
const link = $(el).find(".mbimg a").first().attr("href") || "";
const match = link.match(/\/(video-|hd-porn\/)([^/]+)/);
let id = "";
if (match) {
if (match[1] === "video-") {
id = `video-${match[2]}`;
} else {
// hd-porn
id = match[2];
}
}
return {
link: `https://www.eporner.com${link}`,
id,
title: $(el).find(".mbtit a").text().trim(),
image:
$(el).find("img").attr("data-src") || $(el).find("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: `https://www.eporner.com/embed/${id}`,
};
})
.get()
.filter((v) => v.id && v.image);
return {
success: true,
data: data as unknown as string[],
source: url,
} as ISearchVideoData;
}

View File

@@ -0,0 +1,70 @@
import { load } from "cheerio";
import LustPress from "../../LustPress";
import { ISearchItem } from "../../interfaces";
const lust = new LustPress();
export async function scrapeContent(url: string) {
try {
const res = await lust.fetchBody(url);
const $ = load(res);
class EPornerSearch {
data: ISearchItem[];
constructor() {
this.data = $("#vidresults .mb")
.map((i, el) => {
const link =
$(el).find(".mbimg a").first().attr("href") || "";
const match = link.match(/\/(video-|hd-porn\/)([^/]+)/);
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();
}
}
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

@@ -1,15 +1,13 @@
import { load } from "cheerio"; import { load } from "cheerio";
import LustPress from "../../LustPress"; import { lust } from "../../LustPress";
import { IVideoData } from "../../interfaces"; import { IVideoData } from "../../interfaces";
const lust = new LustPress();
export async function scrapeContent(url: string) { export async function scrapeContent(url: string) {
try { try {
const resolve = await lust.fetchBody(url); const resolve = await lust.fetchBody(url);
const $ = load(resolve); const $ = load(resolve);
class PornHub { class PornHub {
link: string; link: string;
id: string; id: string;
title: string; title: string;
@@ -29,28 +27,29 @@ export async function scrapeContent(url: string) {
this.title = $("meta[property='og:title']").attr("content") || "None"; this.title = $("meta[property='og:title']").attr("content") || "None";
this.image = $("meta[property='og:image']").attr("content") || "None"; this.image = $("meta[property='og:image']").attr("content") || "None";
//get <meta property="video:duration" content=" //get <meta property="video:duration" content="
this.duration = $("meta[property='video:duration']").attr("content") || "0"; this.duration =
$("meta[property='video:duration']").attr("content") || "0";
this.views = $("div.views > span.count").text() || "None"; this.views = $("div.views > span.count").text() || "None";
this.rating = $("div.ratingPercent > span.percent").text() || "None"; this.rating = $("div.ratingPercent > span.percent").text() || "None";
this.videoInfo = $("div.videoInfo").text() || "None"; this.videoInfo = $("div.videoInfo").text() || "None";
this.upVote = $("span.votesUp").attr("data-rating") || "None"; this.upVote = $("span.votesUp").attr("data-rating") || "None";
this.downVote = $("span.votesDown").attr("data-rating") || "None"; this.downVote = $("span.votesDown").attr("data-rating") || "None";
this.video = $("meta[property='og:video:url']").attr("content") || "None"; this.video =
$("meta[property='og:video:url']").attr("content") || "None";
this.tags = $("div.video-info-row") this.tags = $("div.video-info-row")
.find("a") .find("a")
.map((i, el) => { .map((i, el) => {
return $(el).text(); return $(el).text();
}).get(); })
.get();
this.tags.shift(); this.tags.shift();
this.tags = this.tags.map((el) => lust.removeHtmlTagWithoutSpace(el)); this.tags = this.tags.map((el) => lust.removeHtmlTagWithoutSpace(el));
this.models = $("div.pornstarsWrapper.js-pornstarsWrapper") this.models = $("div.pornstarsWrapper.js-pornstarsWrapper a")
.find("a") .map((i, el) => $(el).text().trim())
.map((i, el) => { .get();
return $(el).attr("data-mxptext");
}).get();
} }
} }
const ph = new PornHub(); const ph = new PornHub();
const data: IVideoData = { const data: IVideoData = {
success: true, success: true,
@@ -65,14 +64,14 @@ export async function scrapeContent(url: string) {
upvoted: ph.upVote, upvoted: ph.upVote,
downvoted: ph.downVote, downvoted: ph.downVote,
models: ph.models, models: ph.models,
tags: ph.tags.filter((el) => el !== "Suggest" && el !== " Suggest") tags: ph.tags.filter((el) => el !== "Suggest" && el !== " Suggest"),
}, },
source: ph.link, source: ph.link,
assets: [ph.video, ph.image] assets: [ph.video, ph.image],
}; };
return data; return data;
} catch (err) { } catch (err) {
const e = err as Error; const e = err as Error;
throw Error(e.message); throw Error(e.message);
} }
} }

View File

@@ -1,59 +1,54 @@
import { load } from "cheerio"; import { load } from "cheerio";
import LustPress from "../../LustPress"; import { lust } from "../../LustPress";
import c from "../../utils/options"; import c from "../../utils/options";
import { ISearchVideoData } from "../../interfaces"; import { ISearchVideoData, PornhubSearchItem } from "../../interfaces";
const lust = new LustPress(); export async function scrapeContent(url: string) {
try {
export async function scrapeContent(url: string) { const res = await lust.fetchBody(url);
try { const $ = load(res);
const res = await lust.fetchBody(url);
const $ = load(res); class PornhubSearch {
search: PornhubSearchItem[];
class PornhubSearch { data: PornhubSearchItem[];
search: object[]; constructor() {
data: object; this.search = $("div.wrap")
constructor() { .map((i, el) => {
this.search = $("div.wrap") const link = $(el).find("a").attr("href") || "";
.map((i, el) => { const id = link.split("=")[1] || "None";
const link = $(el).find("a").attr("href"); const title = $(el).find("a").attr("title") || "None";
const id = link?.split("=")[1]; const image = $(el).find("img").attr("src") || "None";
const title = $(el).find("a").attr("title"); const duration = $(el).find("var.duration").text() || "None";
const image = $(el).find("img").attr("src"); const views = $(el).find("div.videoDetailsBlock").find("span.views").text() || "None";
const duration = $(el).find("var.duration").text(); return {
const views = $(el).find("div.videoDetailsBlock").find("span.views").text(); link: `${c.PORNHUB}${link}`,
return { id: id,
link: `${c.PORNHUB}${link}`, title: title,
id: id, image: image,
title: title, duration: duration,
image: image, views: views,
duration: duration, video: `${c.PORNHUB}/embed/${id}`,
views: views, };
video: `${c.PORNHUB}/embed/${id}`, }).get();
};
}).get(); this.data = this.search.filter((el) => {
return el.link.includes("javascript:void(0)") === false && el.image?.startsWith("data:image") === false;
this.data = this.search.filter((el: any) => { });
return el.link.includes("javascript:void(0)") === false && el.image?.startsWith("data:image") === false; }
});
} }
} const ph = new PornhubSearch();
if (ph.search.length === 0) throw Error("No result found");
const ph = new PornhubSearch(); const result: ISearchVideoData = {
if (ph.search.length === 0) throw Error("No result found"); success: true,
const data = ph.data as string[]; data: ph.data as unknown as string[],
const result: ISearchVideoData = { source: url,
success: true, };
data: data, return result;
source: url,
}; } catch (err) {
return result; const e = err as Error;
throw Error(e.message);
}
}
} catch (err) {
const e = err as Error;
throw Error(e.message);
}
}

View File

@@ -1,72 +1,63 @@
import { load } from "cheerio"; import { load } from "cheerio";
import LustPress from "../../LustPress"; import { lust } from "../../LustPress";
import c from "../../utils/options"; import c from "../../utils/options";
import { ISearchVideoData } from "../../interfaces"; import { ISearchVideoData, RedTubeSearchItem } from "../../interfaces";
const lust = new LustPress(); export async function scrapeContent(url: string) {
try {
export async function scrapeContent(url: string) { const res = await lust.fetchBody(url);
try { const $ = load(res);
const res = await lust.fetchBody(url);
const $ = load(res); class RedTubeSearch {
views: string[];
class RedTubeSearch { search: RedTubeSearchItem[];
views: string[]; data: RedTubeSearchItem[];
search: object[]; constructor() {
data: object; this.views = $("span.video_count")
constructor() { .map((i, el) => {
this.views = $("span.video_count") return $(el).text();
.map((i, el) => { }).get();
const views = $(el).text(); this.search = $("a.video_link")
return views; .map((i, el) => {
}).get(); const link = $(el).attr("href") || "";
this.search = $("a.video_link") const id = link.split("/")[1] || "None";
.map((i, el) => { const title = $(el).find("img").attr("alt") || "None";
const link = $(el).attr("href"); const image = $(el).find("img").attr("data-src") || "None";
const id = link?.split("/")[1]; const duration = $(el).find("span.duration").text().split(" ").map((el: string) => {
const title = $(el).find("img").attr("alt"); return el.replace(/[^0-9:]/g, "");
const image = $(el).find("img").attr("data-src"); }).filter((el: string) => {
const duration = $(el).find("span.duration").text().split(" ").map((el: string) => { return el.includes(":");
return el.replace(/[^0-9:]/g, ""); }).join(" ");
}).filter((el: string) => {
return el.includes(":"); return {
}).join(" "); link: `${c.REDTUBE}${link}`,
id: id,
return { title: title,
link: `${c.REDTUBE}${link}`, image: image,
id: id, duration: duration,
title: title, views: this.views[i] || "None",
image: image, video: `https://embed.redtube.com/?id=${id}`,
duration: duration, };
views: this.views[i], }).get();
video: `https://embed.redtube.com/?id=${id}`,
this.data = this.search.filter((el) => {
}; return el.link.includes("javascript:void(0)") === false && el.image?.startsWith("data:image") === false;
}).get(); });
}
}
const red = new RedTubeSearch();
this.data = this.search.filter((el: any) => {
return el.link.includes("javascript:void(0)") === false && el.image?.startsWith("data:image") === false; if (red.search.length === 0) throw Error("No result found");
}); const result: ISearchVideoData = {
} success: true,
data: red.data as unknown as string[],
} source: url,
};
const red = new RedTubeSearch(); return result;
if (red.search.length === 0) throw Error("No result found"); } catch (err) {
const data = red.data as string[]; const e = err as Error;
const result: ISearchVideoData = { throw Error(e.message);
success: true, }
data: data, }
source: url,
};
return result;
} catch (err) {
const e = err as Error;
throw Error(e.message);
}
}

View File

@@ -1,83 +1,110 @@
import { load } from "cheerio"; import { load } from "cheerio";
import LustPress from "../../LustPress"; import { lust } from "../../LustPress";
import { IVideoData } from "../../interfaces"; import { IVideoData, XhamsterInitials } from "../../interfaces";
const lust = new LustPress();
export async function scrapeContent(url: string) { export async function scrapeContent(url: string) {
try { try {
const resolve = await lust.fetchBody(url); const buffer = await lust.fetchBody(url);
const $ = load(resolve); const $ = load(buffer.toString("utf8"));
class Xhamster { const raw = $("#initials-script").html();
const initials = (raw
? JSON.parse(
raw.replace(/^window\.initials\s*=\s*/, "").replace(/;$/, ""),
)
: null) as XhamsterInitials | null;
class Xhamster {
link: string; link: string;
id: string; id: string;
title: string; title: string;
image: string; image: string;
duration: any; duration: string;
views: string; views: string;
rating: string; rating: string;
publish: string;
upVote: string; upVote: string;
downVote: string; downVote: string;
video: string; publish: string;
tags: string[]; tags: string[];
models: string[]; models: string[];
video: string;
constructor() { constructor() {
this.link = $("link[rel='canonical']").attr("href") || "None"; this.link = $("link[rel='canonical']").attr("href") || "None";
this.id = this.link.split("/")[3] + "/" + this.link.split("/")[4] || "None"; this.id = this.link.split("/")[4] || "None";
this.title = $("meta[property='og:title']").attr("content") || "None"; this.title = $("meta[property='og:title']").attr("content") || "None";
this.image = $("meta[property='og:image']").attr("content") || "None"; this.image = $("meta[property='og:image']").attr("content") || "None";
this.duration = $("script#initials-script").html() || "None";
//remove window.initials={ and }; // defaults
this.duration = this.duration.replace("window.initials=", ""); this.duration = "None";
this.duration = this.duration.replace(/;/g, ""); this.views = "None";
this.duration = JSON.parse(this.duration);
this.duration = this.duration.videoModel.duration || "None"; const scripts = $("script")
this.views = $("div.header-icons").find("span").first().text() || "None"; .map((i, el) => $(el).html())
this.rating = $("div.header-icons").find("span").eq(1).text() || "None"; .get()
this.publish = $("div.entity-info-container__date").attr("data-tooltip") || "None"; .filter(Boolean);
this.upVote = $("div.rb-new__info").text().split("/")[0].trim() || "None";
this.downVote = $("div.rb-new__info").text().split("/")[1].trim() || "None"; const videoScript = scripts.find(
this.video = "https://xheve2.com/embed/" + this.link.split("-").pop() || "None"; (s) => s.includes("\"videoModel\"") && s.includes("\"duration\""),
this.tags = $("a.video-tag") );
.map((i, el) => {
return $(el).text(); if (videoScript) {
}).get(); const durMatch = videoScript.match(/"duration"\s*:\s*(\d+)/);
this.tags = this.tags.map((el) => lust.removeHtmlTagWithoutSpace(el)); if (durMatch) this.duration = durMatch[1];
this.models = $("a.video-tag")
.map((i, el) => { const viewMatch = videoScript.match(/"views"\s*:\s*(\d+)/);
return $(el).attr("href"); if (viewMatch) this.views = viewMatch[1];
} }
).get();
this.models = this.models.filter((el) => el.startsWith("https://xheve2.com/pornstars/")); this.rating =
this.models = this.models.map((el) => el.replace("https://xheve2.com/pornstars/", "")); initials?.ratingComponent?.ratingModel?.value?.toString() || "None";
this.upVote =
initials?.ratingComponent?.ratingModel?.likes?.toString() || "None";
this.downVote =
initials?.ratingComponent?.ratingModel?.dislikes?.toString() ||
"None";
this.publish =
$("div.entity-info-container__date").attr("data-tooltip") || "None";
this.tags =
initials?.videoTagsComponent?.tags
?.filter((t) => t.isTag)
.map((t) => t.name) || [];
this.models =
initials?.videoTagsComponent?.tags
?.filter((t) => t.isPornstar)
.map((t) => t.name) || [];
const embedId = this.link.split("-").pop()?.replace("/", "");
this.video = embedId ? `https://xhamster.com/embed/${embedId}` : "None";
} }
} }
const xh = new Xhamster(); const xh = new Xhamster();
const data: IVideoData = {
return {
success: true, success: true,
data: { data: {
title: lust.removeHtmlTagWithoutSpace(xh.title), title: lust.removeHtmlTagWithoutSpace(xh.title),
id: xh.id, id: xh.id,
image: xh.image, image: xh.image,
duration: lust.secondToMinute(Number(xh.duration)), duration: xh.duration,
views: xh.views, views: xh.views,
rating: xh.rating, rating: xh.rating,
uploaded: xh.publish, uploaded: xh.publish,
upvoted: xh.upVote, upvoted: xh.upVote,
downvoted: xh.downVote, downvoted: xh.downVote,
models: xh.models, models: xh.models,
tags: xh.tags tags: xh.tags,
}, },
source: xh.link, source: xh.link,
assets: [xh.video, xh.image] assets: [xh.video, xh.image],
}; } satisfies IVideoData;
return data;
} catch (err) { } catch (err) {
const e = err as Error; const e = err as Error;
throw Error(e.message); throw new Error(e.message);
} }
} }

View File

@@ -1,56 +1,54 @@
import { load } from "cheerio"; import { load } from "cheerio";
import LustPress from "../../LustPress"; import { lust } from "../../LustPress";
import { ISearchVideoData } from "../../interfaces"; import c from "../../utils/options";
import { ISearchVideoData, XhamsterSearchItem } from "../../interfaces";
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 XhamsterSearch {
class XhamsterSearch { search: XhamsterSearchItem[];
search: any; constructor() {
constructor() { const views = $("div.video-thumb-views")
const views = $("div.video-thumb-views") .map((i, el) => {
.map((i, el) => { return $(el).text();
const views = $(el).text(); })
return views; .get();
}).get(); const duration = $("span[data-role='video-duration']")
const duration = $("span[data-role='video-duration']") .map((i, el) => {
.map((i, el) => { return $(el).text();
const duration = $(el).text(); })
return duration; .get();
}).get(); this.search = $("a.video-thumb__image-container")
this.search = $("a.video-thumb__image-container") .map((i, el) => {
.map((i, el) => { const link = $(el).attr("href") || "";
const link = $(el).attr("href");
return {
return { link: link,
link: `${link}`, id: link.split("/")[4] || "None",
id: link?.split("/")[3] + "/" + link?.split("/")[4], title: $(el).find("img").attr("alt") || "None",
title: $(el).find("img").attr("alt"), image: $(el).find("img").attr("src") || "None",
image: $(el).find("img").attr("src"), duration: duration[i] || "None",
duration: duration[i], views: views[i] || "None",
views: views[i], video: `${c.XHAMSTER}/embed/${link.split("-").pop()}`,
video: $(el).attr("data-previewvideo"), };
}; })
}).get(); .get();
} }
} }
const xh = new XhamsterSearch(); const xh = new XhamsterSearch();
if (xh.search.length === 0) throw Error("No result found"); if (xh.search.length === 0) throw Error("No result found");
const data = xh.search as unknown as string[]; const result: ISearchVideoData = {
const result: ISearchVideoData = { success: true,
success: true, data: xh.search as unknown as string[],
data: data, source: url,
source: url, };
}; return result;
return result; } catch (err) {
const e = err as Error;
} catch (err) { throw Error(e.message);
const e = err as Error; }
throw Error(e.message); }
}
}

View File

@@ -6,7 +6,6 @@ const lust = new LustPress();
export async function scrapeContent(url: string) { export async function scrapeContent(url: string) {
try { try {
console.log(url);
const resolve = await lust.fetchBody(url); const resolve = await lust.fetchBody(url);
const $ = load(resolve); const $ = load(resolve);
@@ -27,6 +26,7 @@ export async function scrapeContent(url: string) {
thumbnail: string; thumbnail: string;
bigimg: string; bigimg: string;
video: string; video: string;
embed: string;
constructor() { constructor() {
const thumb = $("script") const thumb = $("script")
.map((i, el) => { .map((i, el) => {
@@ -69,6 +69,8 @@ export async function scrapeContent(url: string) {
.map((i, el) => { .map((i, el) => {
return $(el).text(); return $(el).text();
}).get(); }).get();
this.embed = $("input#copy-video-embed").attr("value") || "None";
this.embed = this.embed.split("iframe")[1].split(" ")[1].replace(/src=/g, "").replace(/"/g, "") || "None";
} }
} }
@@ -91,7 +93,7 @@ export async function scrapeContent(url: string) {
tags: x.tags.filter((el) => el !== "Edit tags and models") tags: x.tags.filter((el) => el !== "Edit tags and models")
}, },
source: x.link, source: x.link,
assets: lust.removeAllSingleQuoteOnArray([x.thumbnail, x.bigimg, x.video]) assets: lust.removeAllSingleQuoteOnArray([x.embed, x.thumbnail, x.bigimg, x.video])
}; };
return data; return data;

View File

@@ -1,55 +1,49 @@
import { load } from "cheerio"; import { load } from "cheerio";
import LustPress from "../../LustPress"; import { lust } from "../../LustPress";
import c from "../../utils/options"; import c from "../../utils/options";
import { ISearchVideoData } from "../../interfaces"; import { ISearchVideoData, XnxxRelatedRaw } from "../../interfaces";
const lust = new LustPress(); export async function scrapeContent(url: string) {
try {
export async function scrapeContent(url: string) { const res = await lust.fetchBody(url);
try { const $ = load(res);
const res = await lust.fetchBody(url);
const $ = load(res); class XnxxSearch {
search: object[];
class PornhubSearch { constructor() {
search: object[]; this.search = $("div#video-player-bg")
data: object; .map((i, el) => {
constructor() { const script = $(el).find("script").html();
// in <div id="video-player-bg"> get <script>var video_related= const video_related = script?.split("var video_related=")[1];
this.search = $("div#video-player-bg") const badJson = video_related?.split("];")[0] + "]";
.map((i, el) => { const actualResult = JSON.parse(String(badJson)) as XnxxRelatedRaw[];
const script = $(el).find("script").html(); const result = actualResult.map((rel) => {
const video_related = script?.split("var video_related=")[1]; return {
//stop and replace everything after the last ]; link: `${c.XNXX}${rel.u}`,
const badJson = video_related?.split("];")[0] + "]"; id: rel.u.slice(1, -1),
const actualResult = JSON.parse(String(badJson)); title: rel.t,
const result = actualResult.map((el: any) => { image: rel.i,
return { duration: rel.d,
link: `${c.XNXX}${el.u}`, views: `${rel.n}, ${rel.r}`,
id: el.u, video: `${c.XNXX}/embedframe/${rel.id}`
title: el.t, };
image: el.i, });
duration: el.d, return result;
views: `${el.n}, ${el.r}`, }).get();
video: null }
}; }
});
return result; const x = new XnxxSearch();
}).get(); if (x.search.length === 0) throw Error("No result found");
} const result: ISearchVideoData = {
} success: true,
data: x.search as unknown as string[],
const x = new PornhubSearch(); source: url,
if (x.search.length === 0) throw Error("No result found"); };
const data = x.search as unknown as string[]; return result;
const result: ISearchVideoData = {
success: true, } catch (err) {
data: data, const e = err as Error;
source: url, throw Error(e.message);
}; }
return result; }
} catch (err) {
const e = err as Error;
throw Error(e.message);
}
}

View File

@@ -17,7 +17,8 @@ export async function scrapeContent(url: string) {
.map((i, el) => { .map((i, el) => {
return { return {
link: `${c.XNXX}${$(el).find("a").attr("href")}`, link: `${c.XNXX}${$(el).find("a").attr("href")}`,
id: $(el).find("a").attr("href"), // remove first "/" and last "/"
id: $(el).find("a").attr("href")?.slice(1, -1),
title: $(el).find("div.thumb-under").text().split("\n") title: $(el).find("div.thumb-under").text().split("\n")
.map((el) => el.trim()).filter((el) => el !== "")[0], .map((el) => el.trim()).filter((el) => el !== "")[0],
image: $(el).find("img").attr("data-src"), image: $(el).find("img").attr("data-src"),
@@ -25,8 +26,7 @@ export async function scrapeContent(url: string) {
.map((el) => el.trim()).filter((el) => el !== "")[2], .map((el) => el.trim()).filter((el) => el !== "")[2],
rating: $(el).find("div.thumb-under").text().split("\n") rating: $(el).find("div.thumb-under").text().split("\n")
.map((el) => el.trim()).filter((el) => el !== "")[1], .map((el) => el.trim()).filter((el) => el !== "")[1],
video: null video: `${c.XNXX}/embedframe/${$(el).find("img").attr("data-videoid")}`
}; };
}).get(); }).get();
} }

View File

@@ -25,6 +25,7 @@ export async function scrapeContent(url: string) {
models: string[]; models: string[];
thumbnail: string; thumbnail: string;
bigimg: string; bigimg: string;
embed: string;
constructor() { constructor() {
this.link = $("meta[property='og:url']").attr("content") || "None"; this.link = $("meta[property='og:url']").attr("content") || "None";
this.id = this.link.split("/")[3] + "/" + this.link.split("/")[4] || "None"; this.id = this.link.split("/")[3] + "/" + this.link.split("/")[4] || "None";
@@ -60,7 +61,8 @@ export async function scrapeContent(url: string) {
} }
).get(); ).get();
this.models = this.models.map((el) => el.split("/")[2]); this.models = this.models.map((el) => el.split("/")[2]);
this.embed = $("input#copy-video-embed").attr("value") || "None";
this.embed = this.embed.split("iframe")[1].split(" ")[1].replace(/src=/g, "").replace(/"/g, "") || "None";
} }
} }
@@ -81,7 +83,7 @@ export async function scrapeContent(url: string) {
tags: xv.tags, tags: xv.tags,
}, },
source: xv.link, source: xv.link,
assets: lust.removeAllSingleQuoteOnArray([xv.thumbnail, xv.bigimg, xv.video]) assets: lust.removeAllSingleQuoteOnArray([xv.embed, xv.thumbnail, xv.bigimg, xv.video])
}; };
return data; return data;

View File

@@ -1,54 +1,49 @@
import { load } from "cheerio"; import { load } from "cheerio";
import LustPress from "../../LustPress"; import { lust } from "../../LustPress";
import c from "../../utils/options"; import c from "../../utils/options";
import { ISearchVideoData } from "../../interfaces"; import { ISearchVideoData, XvideosRelatedRaw } from "../../interfaces";
const lust = new LustPress(); export async function scrapeContent(url: string) {
try {
export async function scrapeContent(url: string) { const res = await lust.fetchBody(url);
try { const $ = load(res);
const res = await lust.fetchBody(url);
const $ = load(res); class XvideosSearch {
console.log(url); search: object[];
constructor() {
class XvideosSearch { this.search = $("div#video-player-bg")
search: object[]; .map((i, el) => {
data: object; const script = $(el).find("script").html();
constructor() { const video_related = script?.split("var video_related=")[1];
this.search = $("div#video-player-bg") const badJson = video_related?.split("];")[0] + "]";
.map((i, el) => { const actualResult = JSON.parse(String(badJson)) as XvideosRelatedRaw[];
const script = $(el).find("script").html(); const result = actualResult.map((rel) => {
const video_related = script?.split("var video_related=")[1]; return {
const badJson = video_related?.split("];")[0] + "]"; link: `${c.XVIDEOS}${rel.u}`,
const actualResult = JSON.parse(String(badJson)); id: rel.u.slice(1, -1),
const result = actualResult.map((el: any) => { title: rel.t,
return { image: rel.i,
link: `${c.XVIDEOS}${el.u}`, duration: rel.d,
id: el.u, views: `${rel.n}, ${rel.r}`,
title: el.t, video: `${c.XVIDEOS}/embedframe/${rel.id}`
image: el.i, };
duration: el.d, });
views: `${el.n}, ${el.r}`, return result;
video: null }).get();
}; }
}); }
return result;
}).get(); const x = new XvideosSearch();
} if (x.search.length === 0) throw Error("No result found");
} const result: ISearchVideoData = {
success: true,
const x = new XvideosSearch(); data: x.search as unknown as string[],
if (x.search.length === 0) throw Error("No result found"); source: url,
const data = x.search as unknown as string[]; };
const result: ISearchVideoData = { return result;
success: true,
data: data, } catch (err) {
source: url, const e = err as Error;
}; throw Error(e.message);
return result; }
}
} catch (err) {
const e = err as Error;
throw Error(e.message);
}
}

View File

@@ -1,66 +1,64 @@
import { load } from "cheerio"; import { load } from "cheerio";
import LustPress from "../../LustPress"; import { lust } from "../../LustPress";
import c from "../../utils/options"; import c from "../../utils/options";
import { ISearchVideoData } from "../../interfaces"; import { ISearchVideoData, XvideosSearchItem } from "../../interfaces";
const lust = new LustPress(); export async function scrapeContent(url: string) {
try {
export async function scrapeContent(url: string) { const res = await lust.fetchBody(url);
try { const $ = load(res);
console.log(url);
const res = await lust.fetchBody(url); class XvideosSearch {
const $ = load(res); search: XvideosSearchItem[];
constructor() {
class XvideosSearch { const data = $("div.thumb-under")
search: object[]; .map((i, el) => {
constructor() { return {
const data = $("div.thumb-under") title: $(el).find("a").attr("title"),
.map((i, el) => { duration: $(el).find("span.duration")
return { .map((i, el) => {
title: $(el).find("a").attr("title"), return $(el).text();
duration: $(el).find("span.duration") }).get()[0],
.map((i, el) => { };
return $(el).text(); }).get();
}).get()[0], this.search = $("div.mozaique.cust-nb-cols")
}; .find("div.thumb")
}).get(); .map((i, el) => {
this.search = $("div.mozaique.cust-nb-cols") const href = $(el).find("a").attr("href") || "None";
.find("div.thumb") const videoId = $(el).find("img").attr("data-videoid");
.map((i, el) => { return {
return { link: `${c.XVIDEOS}${href}`,
link: `${c.XVIDEOS}${$(el).find("a").attr("href")}` || "None", id: href,
id: $(el).find("a").attr("href") || "None", image: $(el).find("img").attr("data-src") || "None",
image: $(el).find("img").attr("data-src") || "None", title: data[i]?.title || "None",
title: data[i].title || "None", duration: data[i]?.duration === data[i + 1]?.duration
duration: data[i].duration === data[i + 1]?.duration ? ""
? "" : data[i]?.duration || "None",
: data[i].duration || "None", rating: null,
rating: null, video: `${c.XVIDEOS}/embedframe/${videoId}`
video: null };
}; }).get();
}).get();
this.search = this.search.filter((el) => {
this.search = this.search.filter((el: any) => { return !el.id.includes("THUMBNUM");
return !el.id.includes("THUMBNUM"); });
}); this.search = this.search.filter((el) => {
this.search = this.search.filter((el: any) => { return el.id.includes("/video");
return el.id.includes("/video"); });
}); }
} }
}
const xv = new XvideosSearch();
const xv = new XvideosSearch(); if (xv.search.length === 0) throw Error("No result found");
if (xv.search.length === 0) throw Error("No result found"); const result: ISearchVideoData = {
const data = xv.search as unknown as string[]; success: true,
const result: ISearchVideoData = { data: xv.search as unknown as string[],
success: true, source: url,
data: data, };
source: url, return result;
};
return result; } catch (err) {
const e = err as Error;
} catch (err) { throw Error(e.message);
const e = err as Error; }
throw Error(e.message); }
}
}

View File

@@ -9,7 +9,7 @@ export async function scrapeContent(url: string) {
const resolve = await lust.fetchBody(url); const resolve = await lust.fetchBody(url);
const $ = load(resolve); const $ = load(resolve);
class YouPorn { class YouPorn {
link: string; link: string;
id: string; id: string;
title: string; title: string;
@@ -19,33 +19,43 @@ export async function scrapeContent(url: string) {
rating: string; rating: string;
publish: string; publish: string;
upVote: string; upVote: string;
downVote: null; downVote: string;
video: string; video: string;
tags: string[]; tags: string[];
models: string[]; models: string[];
constructor() { constructor() {
this.link = $("link[rel='canonical']").attr("href") || "None"; this.link = $("link[rel='canonical']").attr("href") || "None";
this.id = this.link.replace("https://www.youporn.com/watch/", "") || "None"; this.id =
this.link.replace("https://www.youporn.com/watch/", "") || "None";
this.title = $("meta[property='og:title']").attr("content") || "None"; this.title = $("meta[property='og:title']").attr("content") || "None";
this.image = $("meta[property='og:image']").attr("content") || "None"; this.image = $("meta[property='og:image']").attr("content") || "None";
this.duration = $("meta[property='video:duration']").attr("content") || "0"; this.duration =
this.views = $("div.feature.infoValueBlock").find("div[data-value]").attr("data-value") || "0"; $("meta[property='video:duration']").attr("content") || "0";
this.rating = $("div.feature").find("span").text().replace(/[^0-9.,%]/g, "") || "0"; this.publish =
this.publish = $("div.video-uploaded").find("span").text() || "None"; $("span.publishedDate").text().replace("Published on", "").trim() ||
this.upVote = this.views; "None";
this.downVote = null;
this.upVote = "None";
this.downVote = "None";
this.video = `https://www.youporn.com/embed/${this.id}`; this.video = `https://www.youporn.com/embed/${this.id}`;
this.tags = $("a[data-espnode='category_tag'], a[data-espnode='porntag_tag']") this.views = $("span.tm_infoValue").first().text() || "0";
.map((i, el) => { this.rating = $("span.tm_rating_percent").text() || "0";
return $(el).text();
}).get(); this.tags = $(
this.models = $("a[data-espnode='pornstar_tag']") "div.js_scrollableContent a.bubble-porntag, \
.map((i, el) => { a[data-espnode='category_tag'], \
return $(el).text(); a[data-espnode='porntag_tag']",
}).get(); )
.map((i, el) => $(el).text().trim())
.get()
.filter(Boolean);
this.models = $("#metaDataPornstarInfo a.tm_pornstar_link span")
.map((i, el) => $(el).text().replace(",", "").trim())
.get();
} }
} }
const yp = new YouPorn(); const yp = new YouPorn();
const data: IVideoData = { const data: IVideoData = {
success: true, success: true,
@@ -60,14 +70,14 @@ export async function scrapeContent(url: string) {
upvoted: yp.upVote, upvoted: yp.upVote,
downvoted: yp.downVote, downvoted: yp.downVote,
models: yp.models, models: yp.models,
tags: yp.tags tags: yp.tags,
}, },
source: yp.link, source: yp.link,
assets: [yp.video, yp.image] assets: [yp.video, yp.image],
}; };
return data; return data;
} catch (err) { } catch (err) {
const e = err as Error; const e = err as Error;
throw Error(e.message); throw Error(e.message);
} }
} }

View File

@@ -1,41 +1,82 @@
import { load } from "cheerio"; import { load } from "cheerio";
import LustPress from "../../LustPress"; import { lust } from "../../LustPress";
import c from "../../utils/options"; import c from "../../utils/options";
import { ISearchVideoData } from "../../interfaces"; import { ISearchVideoData } from "../../interfaces";
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 YouPornSearch { class YouPornSearch {
dur: string[]; links: string[];
ids: string[];
titles: string[];
images: string[];
durations: string[];
views: string[];
search: object[]; search: object[];
constructor() { constructor() {
this.dur = $("div.video-duration").map((i, el) => { const cards = $("div.video-box.pc");
return $(el).text();
}).get(); this.links = cards
this.search = $("a[href^='/watch/']")
.map((i, el) => { .map((i, el) => {
const link = $(el).attr("href"); return $(el).find("a.tm_video_link").attr("href");
const id = `${link}`.split("/")[2] + "/" + `${link}`.split("/")[3]; })
const title = $(el).find("div.video-box-title").text(); .get();
const image = $(el).find("img").attr("data-thumbnail");
this.ids = this.links.map((link) => link?.split("/")[2]);
this.titles = cards
.map((i, el) => {
return $(el).find("a.tm_video_title span").text().trim();
})
.get();
this.images = cards
.map((i, el) => {
return (
$(el).find("img.thumb-image").attr("data-src") ||
$(el).find("img.thumb-image").attr("src")
);
})
.get();
this.durations = cards
.map((i, el) => {
return $(el).find("div.tm_video_duration span").text().trim();
})
.get();
this.views = cards
.map((i, el) => {
return (
$(el)
.find(".view-rating-container .info-views")
.first()
.text()
.trim() || "None"
);
})
.get();
this.search = cards
.map((i) => {
return { return {
link: `${c.YOUPORN}${link}`, link: `${c.YOUPORN}${this.links[i]}`,
id: id, id: this.ids[i],
title: lust.removeHtmlTagWithoutSpace(title), title: lust.removeHtmlTagWithoutSpace(this.titles[i]),
image: image, image: this.images[i],
duration: this.dur[i], duration: this.durations[i],
views: null, views: this.views[i],
video: `https://www.youporn.com/embed/${id}`, video: `https://www.youporn.com/embed/${this.ids[i]}`,
}; };
}).get(); })
.get();
} }
} }
const yp = new YouPornSearch(); const yp = new YouPornSearch();
if (yp.search.length === 0) throw Error("No result found"); if (yp.search.length === 0) throw Error("No result found");
const data = yp.search as unknown as string[]; const data = yp.search as unknown as string[];
@@ -45,9 +86,8 @@ export async function scrapeContent(url: string) {
source: url, source: url,
}; };
return result; return result;
} catch (err) { } catch (err) {
const e = err as Error; const e = err as Error;
throw Error(e.message); throw Error(e.message);
} }
} }

View File

@@ -1,17 +0,0 @@
import rateLimit from "express-rate-limit";
import slowDown from "express-slow-down";
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 50,
message: "Too nasty, please slow down"
});
const slow = slowDown({
delayAfter: 50,
windowMs: 15 * 60 * 1000,
delayMs: 1000,
maxDelayMs: 20000,
});
export { limiter, slow };

View File

@@ -1,8 +0,0 @@
import pino from "pino";
export const logger = pino({
level: "info",
transport: {
target: "pino-pretty"
},
});

View File

@@ -19,7 +19,7 @@ export function maybeError(success: boolean, message: string) {
export function timeAgo(input: Date) { export function timeAgo(input: Date) {
const date = new Date(input); const date = new Date(input);
const formatter: any = new Intl.RelativeTimeFormat("en"); const formatter = new Intl.RelativeTimeFormat("en");
const ranges: { [key: string]: number } = { const ranges: { [key: string]: number } = {
years: 3600 * 24 * 365, years: 3600 * 24 * 365,
months: 3600 * 24 * 30, months: 3600 * 24 * 30,
@@ -33,7 +33,7 @@ export function timeAgo(input: Date) {
for (const key in ranges) { for (const key in ranges) {
if (ranges[key] < Math.abs(secondsElapsed)) { if (ranges[key] < Math.abs(secondsElapsed)) {
const delta = secondsElapsed / ranges[key]; const delta = secondsElapsed / ranges[key];
return formatter.format(Math.round(delta), key); return formatter.format(Math.round(delta), key as Intl.RelativeTimeFormatUnit);
} }
} }
} }

View File

@@ -1,9 +1,11 @@
export default { export default {
EPORNER: "https://www.eporner.com",
PORNHUB: "https://www.pornhub.com", PORNHUB: "https://www.pornhub.com",
XNXX: "https://www.xnxx.com", XNXX: "https://www.xnxx.com",
REDTUBE: "https://www.redtube.com", REDTUBE: "https://www.redtube.com",
XVIDEOS: "https://www.xvideos.com", XVIDEOS: "https://www.xvideos.com",
XHAMSTER: "https://xheve2.com", XHAMSTER: "https://xhamster.com/",
YOUPORN: "https://www.youporn.com", YOUPORN: "https://www.youporn.com",
TXXX: "https://txxx.com/",
JAVHD: "https://javhd.today" JAVHD: "https://javhd.today"
}; };

83
src/utils/ph-solver.ts Normal file
View File

@@ -0,0 +1,83 @@
/**
* solve Pornhub JS challenge (leastFactor)
* @param html challenge page html
* @returns KEY cookie string
*/
export async function solveChallenge(html: string): Promise<string> {
const cleanHtml = html.replace(/\/\*[\s\S]*?\*\//g, "");
const pMatch = cleanHtml.match(/var p=(\d+);/);
const sMatch = cleanHtml.match(/var s=(\d+);/);
const extraMatch = cleanHtml.match(
/document\.cookie="KEY="\+n\+"\*"\+p\/n\+":"\+s\+":(\d+):1;path=\/;";/
);
if (!pMatch || !sMatch || !extraMatch) {
throw new Error("Challenge variables not found");
}
let p = parseInt(pMatch[1]);
const s = parseInt(sMatch[1]);
const extra = extraMatch[1];
const goFuncMatch = cleanHtml.match(
/function go\(\) \{([\s\S]+?)n=leastFactor\(p\);/
);
if (goFuncMatch) {
const body = goFuncMatch[1].replace(/\s+/g, "");
// 1. Find and process all if-else blocks, then REMOVE them from body
const ifElseRegex = /if\(\(s>>(\d+)\)&1\)p(\+|-)=(\d+)\*(\d+);elsep(\+|-)=(\d+)\*(\d+);/g;
let match;
let bodyWithoutIfs = body;
while ((match = ifElseRegex.exec(body)) !== null) {
const shift = parseInt(match[1]);
const condition = (s >> shift) & 1;
if (condition) {
const sign = match[2];
const val = parseInt(match[3]) * parseInt(match[4]);
p = sign === "+" ? p + val : p - val;
} else {
const sign = match[5];
const val = parseInt(match[6]) * parseInt(match[7]);
p = sign === "+" ? p + val : p - val;
}
// Remove the matched if-else block to avoid double counting
bodyWithoutIfs = bodyWithoutIfs.replace(match[0], "");
}
// 2. Now process remaining adjustments (p+=... or p-=...) in the cleaned body
const adjRegex = /p(\+|-)=(\d+);/g;
let adjMatch;
while ((adjMatch = adjRegex.exec(bodyWithoutIfs)) !== null) {
const sign = adjMatch[1];
const val = parseInt(adjMatch[2]);
p = sign === "+" ? p + val : p - val;
}
}
const n = leastFactor(p);
return `KEY=${n}*${p / n}:${s}:${extra}:1`;
}
function leastFactor(n: number): number {
if (isNaN(n) || !isFinite(n)) return NaN;
if (n === 0) return 0;
if (n % 1 || n * n < 2) return 1;
if (n % 2 === 0) return 2;
if (n % 3 === 0) return 3;
if (n % 5 === 0) return 5;
const m = Math.sqrt(n);
for (let i = 7; i <= m; i += 30) {
if (n % i === 0) return i;
if (n % (i + 4) === 0) return i + 4;
if (n % (i + 6) === 0) return i + 6;
if (n % (i + 10) === 0) return i + 10;
if (n % (i + 12) === 0) return i + 12;
if (n % (i + 16) === 0) return i + 16;
if (n % (i + 22) === 0) return i + 22;
if (n % (i + 24) === 0) return i + 24;
}
return n;
}

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

@@ -0,0 +1,57 @@
import { test } from "bun:test";
import assert from "node:assert/strict";
const port = process.env.PORT ?? 3000;
type ApiResponse = {
success: boolean;
data?: {
id?: string | number;
};
};
async function run(path: string) {
const res = await fetch(`http://localhost:${port}${path}`);
assert.equal(res.status, 200);
const json = (await res.json()) as ApiResponse;
console.log(JSON.stringify(json, null, 2));
if (!json.success) {
throw new Error("scraper failed");
}
}
test("pornhub", async () => {
await run("/pornhub/random");
}, 300000);
test("xnxx", async () => {
await run("/xnxx/random");
}, 120000);
test("redtube", async () => {
await run("/redtube/random");
}, 120000);
test("xvideos", async () => {
await run("/xvideos/random");
}, 120000);
test("xhamster", async () => {
await run("/xhamster/random");
}, 120000);
test("youporn", async () => {
await run("/youporn/random");
}, 120000);
test("eporner", async () => {
await run("/eporner/random");
}, 120000);
test("txxx", async () => {
await run("/txxx/random");
}, 120000);

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

@@ -2,7 +2,7 @@ import c from "../src/utils/options";
import p from "phin"; import p from "phin";
for (const url of for (const url of
[c.PORNHUB, c.XNXX, c.REDTUBE, c.XVIDEOS, c.XHAMSTER, c.YOUPORN]) { [c.PORNHUB, c.XNXX, c.REDTUBE, c.XVIDEOS, c.XHAMSTER, c.YOUPORN, c.EPORNER, c.TXXX]) {
p({ url }).then(res => { p({ url }).then(res => {
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
console.log(`${url} is not available, status code: ${res.statusCode}, check the sites or your own user-agent`); console.log(`${url} is not available, status code: ${res.statusCode}, check the sites or your own user-agent`);

View File

@@ -1,24 +1,24 @@
{ {
"$schema": "http://json.schemastore.org/tsconfig", "$schema": "http://json.schemastore.org/tsconfig",
"compilerOptions": { "compilerOptions": {
"outDir": "./build", "lib": ["ESNext"],
"allowJs": true, "module": "ESNext",
"target": "ESNext", "target": "ESNext",
"baseUrl": "src", "moduleResolution": "bundler",
"resolveJsonModule": true, "moduleDetection": "force",
"esModuleInterop": true, "allowJs": true,
"allowSyntheticDefaultImports": true, "allowImportingTsExtensions": true,
"moduleResolution": "node", "noEmit": true,
"module": "commonjs", "composite": true,
"paths": {},
"typeRoots": ["./node_modules/@types"],
"inlineSourceMap": true,
"downlevelIteration": true,
"newLine": "lf",
"strict": true, "strict": true,
"strictBindCallApply": true, "downlevelIteration": true,
"strictPropertyInitialization": false, "skipLibCheck": true,
"declaration": true "jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowUnreachableCode": false,
"baseUrl": "src",
"paths": {}
}, },
"include": [ "include": [
"src/**/*" "src/**/*"