14 Commits

Author SHA1 Message Date
SNEK WAIFU
a091e89e8a auto release 2026-05-14 04:42:44 +07:00
SNEK WAIFU
b1d6add110 chore(release): 8.2.0-alpha 2026-05-14 04:41:27 +07:00
SNEK WAIFU
f04bbead08 feat(core): hard remake with bun and elysia 2026-05-14 04:37:39 +07:00
SNEK WAIFU
07080d4a6a docs changes 2026-05-14 04:37:03 +07:00
SNEK WAIFU
4735defbb4 ci changes use bun 2026-05-14 04:35:19 +07:00
SNEK WAIFU
2cd8e1ed29 remake tsconfig 2026-05-14 04:34:47 +07:00
SNEK WAIFU
fdf634dce7 @elysiajs/swagger 2026-05-14 04:34:34 +07:00
SNEK WAIFU
423c955a18 remake getServer 2026-05-14 04:34:01 +07:00
SNEK WAIFU
ad81dbfcd0 edit proper interfaces 2026-05-14 04:33:31 +07:00
SNEK WAIFU
a925544508 deps and rule changes 2026-05-14 04:31:20 +07:00
SNEK WAIFU
3a13143d8c some docs changes 2026-05-14 04:29:20 +07:00
SNEK WAIFU
2df31521d2 test case 2026-05-14 04:28:31 +07:00
SNEK WAIFU
3cc0c3317f remake modifiers 2026-05-14 04:28:10 +07:00
SNEK WAIFU
11b1228c5d static files changes 2026-05-14 04:17:29 +07:00
76 changed files with 1995 additions and 2581 deletions

View File

@@ -10,24 +10,24 @@ permissions:
contents: read contents: read
jobs: jobs:
test: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Bun
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with: with:
node-version: 24 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

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set image name - name: Set image name
run: echo "IMAGE_NAME=ghcr.io/$GITHUB_REPOSITORY" >> $GITHUB_ENV run: echo "IMAGE_NAME=ghcr.io/$GITHUB_REPOSITORY" >> $GITHUB_ENV

View File

@@ -14,23 +14,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Bun
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with: with:
node-version: 24 bun-version: latest
- name: Install dependencies - name: Install dependencies
run: npm install run: bun install
- name: Install Playwright browsers - name: Run test
run: npx playwright install --with-deps chromium run: bun run test:eporner
- name: Build
run: npm run build
- name: Eporner test
run: npm run test:eporner

View File

@@ -1,37 +0,0 @@
name: Playground
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
permissions:
contents: write
jobs:
build-and-deploy:
concurrency: ci-${{ github.ref }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version: 24
- name: Install dependencies
run: npm install
- name: Generating docs
run: npm run build:apidoc
- name: Deploy
uses: JamesIves/github-pages-deploy-action@9d877eea73427180ae43cf98e8914934fe157a1a
with:
branch: gh-pages
folder: docs

View File

@@ -14,23 +14,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Bun
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with: with:
node-version: 24 bun-version: latest
- name: Install dependencies - name: Install dependencies
run: npm install run: bun install
- name: Install Playwright browsers - name: Run test
run: npx playwright install --with-deps chromium run: bun run test:pornhub
- name: Build
run: npm run build
- name: Pornhub test
run: npm run test:pornhub

View File

@@ -14,23 +14,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Bun
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with: with:
node-version: 24 bun-version: latest
- name: Install dependencies - name: Install dependencies
run: npm install run: bun install
- name: Install Playwright browsers - name: Run test
run: npx playwright install --with-deps chromium run: bun run test:redtube
- name: Build
run: npm run build
- name: Redtube test
run: npm 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

@@ -14,20 +14,20 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Bun
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with: with:
node-version: 24 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 test

View File

@@ -14,23 +14,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Bun
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with: with:
node-version: 24 bun-version: latest
- name: Install dependencies - name: Install dependencies
run: npm install run: bun install
- name: Install Playwright browsers - name: Run test
run: npx playwright install --with-deps chromium run: bun run test:txxx
- name: Build
run: npm run build
- name: Txxx test
run: npm run test:txxx

View File

@@ -14,23 +14,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/checkout@11bd71901bbe5b1630ea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Bun
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with: with:
node-version: 24 bun-version: latest
- name: Install dependencies - name: Install dependencies
run: npm install run: bun install
- name: Install Playwright browsers - name: Run test
run: npx playwright install --with-deps chromium run: bun run test:xhamster
- name: Build
run: npm run build
- name: Xhamster test
run: npm run test:xhamster

View File

@@ -14,23 +14,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Bun
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with: with:
node-version: 24 bun-version: latest
- name: Install dependencies - name: Install dependencies
run: npm install run: bun install
- name: Install Playwright browsers - name: Run test
run: npx playwright install --with-deps chromium run: bun run test:xnxx
- name: Build
run: npm run build
- name: Xnxx test
run: npm run test:xnxx

View File

@@ -14,23 +14,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Bun
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with: with:
node-version: 24 bun-version: latest
- name: Install dependencies - name: Install dependencies
run: npm install run: bun install
- name: Install Playwright browsers - name: Run test
run: npx playwright install --with-deps chromium run: bun run test:xvideos
- name: Build
run: npm run build
- name: Xvideos test
run: npm run test:xvideos

View File

@@ -14,23 +14,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Bun
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with: with:
node-version: 24 bun-version: latest
- name: Install dependencies - name: Install dependencies
run: npm install run: bun install
- name: Install Playwright browsers - name: Run test
run: npx playwright install --with-deps chromium run: bun run test:youporn
- name: Build
run: npm run build
- name: Youporn test
run: npm run test:youporn

1
.gitignore vendored
View File

@@ -10,3 +10,4 @@ p.ts
.idea .idea
CHANGELOG.md CHANGELOG.md
theme.zip theme.zip
AGENTS.md

View File

@@ -4,7 +4,7 @@ I hope you have found this project useful. All the major credit really goes to t
allows this services to operate. allows this services to operate.
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)

View File

@@ -1,17 +1,26 @@
FROM node:22 FROM oven/bun:1.3.13-alpine AS base
WORKDIR /srv/app WORKDIR /srv/app
COPY package*.json ./ # 1. Install production dependencies only
RUN npm install FROM base AS deps
COPY package.json bun.lock ./
# install browser + system deps RUN bun install --production
RUN npx playwright install --with-deps chromium
# 2. Builder stage to compile TypeScript
FROM base AS builder
COPY package.json bun.lock ./
RUN bun install
COPY . . COPY . .
RUN bun run build
RUN npm 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"]

View File

@@ -7,7 +7,7 @@
<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** a high-performance remake from the ground up using Bun.
The motivation of this project is to bring you an actionable data related to pornhub and other r18 sites with gather, similar design pattern, endpoint bindings, and consistent structure in mind. The motivation of this project is to bring you an actionable data related to pornhub and other r18 sites with gather, similar design pattern, endpoint bindings, and consistent structure in mind.
<a href="https://sinkaroid.github.io/lustpress">Playground</a> • <a href="https://sinkaroid.github.io/lustpress">Playground</a> •
@@ -17,7 +17,7 @@ The motivation of this project is to bring you an actionable data related to por
--- ---
<a href="http://localhost:3000/"><img align="right" src="resources/project/images/bnnuy.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)
@@ -32,11 +32,10 @@ The motivation of this project is to bring you an actionable data related to por
- [Playground](https://sinkaroid.github.io/lustpress) - [Playground](https://sinkaroid.github.io/lustpress)
- [Routing](#playground) - [Routing](#playground)
- [Status response](#status-response) - [Status response](#status-response)
- [Pornhub JS 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](#client-libraries)
- [Acknowledgements](#acknowledgements)
- [Legal](#legal) - [Legal](#legal)
- [Discontinued playground](#frequently-asked-questions) - [Discontinued playground](#frequently-asked-questions)
- [again, discontinued playground](#frequently-asked-questions) - [again, discontinued playground](#frequently-asked-questions)
@@ -81,7 +80,7 @@ Some tests may fail in CI environments because certain websites restrict or bloc
## Prerequisites ## Prerequisites
<table> <table>
<td><b>NOTE:</b> NodeJS 22.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.
@@ -90,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
@@ -103,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/8.0.1 Node.js/22.22.0" USER_AGENT = "lustpress/8.2.0-alpha Bun/1.3.13"
``` ```
### Docker ### Docker
@@ -118,7 +114,7 @@ docker run -d \
-p 3028: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/8.0.1 Node.js/22.22.0' \ -e USER_AGENT='lustpress/8.2.0-alpha Bun/1.3.13' \
ghcr.io/sinkaroid/lustpress:latest ghcr.io/sinkaroid/lustpress:latest
``` ```
@@ -127,43 +123,42 @@ 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 ## Running tests
Run the following commands to execute tests for each supported source: Run the following commands to execute tests for each supported source:
```bash ```bash
# Check whether all supported sites are available for scraping # Check whether all supported sites are available for scraping
npm run test bun run test
# Check whether ph and (maybe the others do..) do Solving challenge in their website # Check whether ph and (maybe the others do..) do Solving challenge in their website
npm run test:mock bun run test:mock
# Run tests for individual sources # Run tests for individual sources
npm run test:pornhub bun run test:pornhub
npm run test:xnxx bun run test:xnxx
npm run test:redtube bun run test:redtube
npm run test:xvideos bun run test:xvideos
npm run test:xhamster bun run test:xhamster
npm run test:youporn bun run test:youporn
npm run test:eporner bun run test:eporner
npm run test:txxx bun run test:txxx
``` ```
### 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`
### Generating playground like swagger from apidoc definition ### Generating playground like swagger from apidoc definition
`npm run build:apidoc` `bun run build:apidoc`
> To running other tests, you can see object scripts in file `package.json` or modify the `lustpress.test.ts` according your needs > To running other tests, you can see object scripts in file `package.json` or modify the `lustpress.test.ts` according your needs
@@ -298,6 +293,21 @@ https://sinkaroid.github.io/lustpress/#api-txxx
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.
@@ -311,13 +321,6 @@ To continue using Lustpress, please deploy and run your own self-hosted instance
`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
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)
## 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.

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=="],
}
}

View File

@@ -21,6 +21,14 @@ module.exports = [
process: "readonly", process: "readonly",
Buffer: "readonly", Buffer: "readonly",
console: "readonly", console: "readonly",
Bun: "readonly",
fetch: "readonly",
describe: "readonly",
test: "readonly",
expect: "readonly",
beforeAll: "readonly",
afterAll: "readonly",
it: "readonly",
}, },
}, },
plugins: { plugins: {

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,34 +1,24 @@
{ {
"name": "lustpress", "name": "lustpress",
"version": "8.1.3-alpha", "version": "8.2.0-alpha",
"description": "RESTful and experimental API for PH and other R18 websites", "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", "test:xhamster": "bun test -t xhamster test/lustpress.test.ts",
"build:apidoc": "npm run build:freshdoc && npx apidoc -i src -o docs", "test:youporn": "bun test -t youporn test/lustpress.test.ts",
"test:pornhub": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^pornhub$ test/lustpress.test.ts\"", "test:eporner": "bun test -t eporner test/lustpress.test.ts",
"test:xnxx": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^xnxx$ test/lustpress.test.ts\"", "test:txxx": "bun test -t txxx test/lustpress.test.ts",
"test:redtube": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^redtube$ test/lustpress.test.ts\"", "lint": "eslint .",
"test:xvideos": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^xvideos$ test/lustpress.test.ts\"", "lint:fix": "eslint . --fix"
"test:xhamster": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^xhamster$ test/lustpress.test.ts\"",
"test:youporn": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^youporn$ test/lustpress.test.ts\"",
"test:eporner": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^eporner$ test/lustpress.test.ts\"",
"test:txxx": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^txxx$ test/lustpress.test.ts\""
},
"apidoc": {
"title": "Lustpress API Documentation",
"url": "http://localhost:3000",
"sampleUrl": "http://localhost:3000",
"name": "Lustpress"
}, },
"keywords": [], "keywords": [],
"author": "sinkaroid", "author": "sinkaroid",
@@ -38,34 +28,22 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@elysiajs/cors": "^1.2.0",
"@elysiajs/swagger": "^1.2.2",
"@keyv/redis": "^5.1.6", "@keyv/redis": "^5.1.6",
"cheerio": "^1.2.0", "cheerio": "^1.2.0",
"cors": "^2.8.6", "elysia": "^1.2.24",
"dotenv": "^17.3.1", "keyv": "^5.6.0"
"express": "^5.2.1",
"express-rate-limit": "^8.3.1",
"express-slow-down": "^3.1.0",
"keyv": "^5.6.0",
"phin": "^3.7.1",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"playwright": "^1.58.2"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.19", "@types/bun": "^1.3.13",
"@types/express": "^5.0.6", "@typescript-eslint/eslint-plugin": "^8.24.1",
"@types/node": "^25.4.0", "@typescript-eslint/parser": "^8.24.1",
"@typescript-eslint/eslint-plugin": "^8.57.0", "eslint": "^9.20.1",
"@typescript-eslint/parser": "^8.57.0", "typescript": "^6.0.3"
"apidoc": "^1.2.0",
"eslint": "^10.0.3",
"rimraf": "^6.1.3",
"start-server-and-test": "^2.1.5",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"typescript": "5.9.3"
}, },
"engines": { "engines": {
"node": ">=22" "bun": ">=1.3.13"
} },
"packageManager": "bun@1.3.13"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,203 +1,135 @@
import { URL } from "node:url"; import { URL } from "node:url";
import { chromium } from "playwright";
import p, { IResponse } from "phin";
import Keyv from "keyv"; import Keyv from "keyv";
import KeyvRedis from "@keyv/redis"; 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 const keyv = process.env.REDIS_URL
? new Keyv({ store: new KeyvRedis(process.env.REDIS_URL) }) ? new Keyv({ store: new KeyvRedis(process.env.REDIS_URL) })
: new Keyv(); : new Keyv();
keyv.on("error", err => console.log("Connection Error", err)); keyv.on("error", (err) => console.log("Connection Error", err));
const ttl = 1000 * 60 * 60 * Number(process.env.EXPIRE_CACHE); const ttl = 1000 * 60 * 60 * Number(process.env.EXPIRE_CACHE);
class LustPress { const GEO_TIMEOUT_MS = 3000;
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 {
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 } = {}; private cookieCache: { [domain: string]: string } = {};
constructor() { constructor() {
this.url = ""; this.url = "";
this.useragent = `${pkg.name}/${pkg.version} Node.js/${process.versions.node}`; this.useragent = `${pkg.name}/${pkg.version} Bun/${Bun.version}`;
} }
async getCookies(url: string): Promise<string> { async getPornhubCookies(): Promise<string> {
const browser = await chromium.launch({ headless: true }); 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"
};
const context = await browser.newContext({ console.log("Fetching Pornhub Index for cookies...");
userAgent: process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/${process.versions.node}` let res = await fetch(baseUrl, { headers, redirect: "follow" });
});
const page = await context.newPage(); // Get cookies from set-cookie headers
await page.goto(url, { waitUntil: "domcontentloaded" }); const initialCookies = res.headers.getSetCookie().map(c => c.split(";")[0]).join("; ");
await page.waitForLoadState("networkidle"); let text = await res.text();
const cookies = await context.cookies();
await browser.close();
return cookies.map(c => `${c.name}=${c.value}`).join("; ");
}
/** if (text.includes("leastFactor")) {
* Fetch body from url and check if it's cached console.log("Pornhub Challenge detected at Index. Solving...");
* @param url url to fetch const challengeCookie = await solveChallenge(text);
* @returns Buffer const combinedCookies = [initialCookies, challengeCookie, "age_verified=1"].filter(Boolean).join("; ");
*/
async fetchBody(url: string): Promise<Buffer> {
const cached = await keyv.get(url); // Verify cookies
console.log("Verifying cookies at Index...");
if (cached) { res = await fetch(baseUrl, {
console.log("Fetching from cache"); headers: { ...headers, Cookie: combinedCookies },
return cached; redirect: "follow"
} else if (url.includes("/random")) {
console.log("Random should not be cached");
const isPornhub = /pornhub\.com/i.test(url);
let cookieHeader = "";
if (isPornhub) {
const domain = new URL(url).hostname;
if (this.cookieCache[domain]) {
console.log("Using cached cookie");
cookieHeader = this.cookieCache[domain];
} else {
console.log("Solving challenge via playwright");
cookieHeader = await this.getCookies(url);
this.cookieCache[domain] = cookieHeader;
}
}
console.log(`Fetching from ${isPornhub ? "phin (cookie)" : "phin"}`);
const res = await p({
url: url,
headers: {
"User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/${process.versions.node}`,
...(cookieHeader && { "Cookie": cookieHeader })
},
followRedirects: true
}); });
text = await res.text();
if (isPornhub && res.statusCode !== 200) { if (!text.includes("leastFactor")) {
const domain = new URL(url).hostname; console.log("Pornhub cookies verified!");
console.log("Cookie invalid, clearing cache and retrying via playwright"); return combinedCookies;
delete this.cookieCache[domain]; } else {
const newCookie = await this.getCookies(url); console.log("Warning: Challenge still present after solve attempt.");
this.cookieCache[domain] = newCookie; return combinedCookies;
const retry = await p({
url: url,
headers: {
"User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/${process.versions.node}`,
"Cookie": newCookie
},
followRedirects: true
});
return retry.body;
} }
return res.body;
} else {
console.log("Fetching from source");
url = url.replace(/\/\//g, "/");
const isPornhub = /pornhub\.com/i.test(url);
let cookieHeader = "";
if (isPornhub) {
const domain = new URL(url).hostname;
if (this.cookieCache[domain]) {
console.log("Using cached cookie");
cookieHeader = this.cookieCache[domain];
} else {
console.log("Solving challenge via playwright");
cookieHeader = await this.getCookies(url);
this.cookieCache[domain] = cookieHeader;
}
}
console.log(`Fetching from ${isPornhub ? "phin (cookie)" : "phin"}`);
const res = await p({
url: url,
headers: {
"User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/16.9.1`,
...(cookieHeader && { "Cookie": cookieHeader })
},
followRedirects: true
});
if (isPornhub && res.statusCode !== 200) {
const domain = new URL(url).hostname;
console.log("Cookie invalid, clearing cache and retrying via playwright");
delete this.cookieCache[domain];
const newCookie = await this.getCookies(url);
this.cookieCache[domain] = newCookie;
const retry = await p({
url: url,
headers: {
"User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/16.9.1`,
"Cookie": newCookie
},
followRedirects: true
});
return retry.body;
}
await keyv.set(url, res.body, ttl);
return res.body;
} }
return initialCookies || "age_verified=1";
} }
/** async fetchBody(url: string): Promise<Buffer> {
* remove html tag and bunch of space const cached = await keyv.get(url);
* @param str string to remove html tag if (cached) return cached;
* @returns string
*/ 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";
}
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
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, "");
str = str.replace(/\s+/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,
@@ -205,56 +137,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

@@ -1,45 +1,9 @@
import { scrapeContent } from "../../scraper/eporner/epornerGetController"; import { scrapeContent } from "../../scraper/eporner/epornerGetController";
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 getEporner(req: Request, res: Response) { export async function getEporner({ 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} /eporner/get?id=:id Get eporner
* @apiName Get eporner
* @apiGroup eporner
* @apiDescription Get a eporner 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/eporner/get?id=ibvqvezXzcs
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/eporner/get?id=ibvqvezXzcs")
* .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/eporner/get?id=ibvqvezXzcs") as resp:
* print(await resp.json())
*/
// https://www.eporner.com/video-ibvqvezXzcs/ivy-seduces-her-dad-s-friend/
// https://www.eporner.com/hd-porn/hgovoiPexQe/Risa-Tsukino/
let path: string; let path: string;
if (id.startsWith("video-")) { if (id.startsWith("video-")) {
@@ -51,16 +15,10 @@ export async function getEporner(req: Request, res: Response) {
const url = `${c.EPORNER}/${path}`; const url = `${c.EPORNER}/${path}`;
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,28 +1,15 @@
import { scrapeContent } from "../../scraper/eporner/epornerGetRelatedController"; import { scrapeContent } from "../../scraper/eporner/epornerGetRelatedController";
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 relatedEporner(req: Request, res: Response) { export async function relatedEporner({ 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.EPORNER}/video-${id}`; const url = `${c.EPORNER}/video-${id}`;
const data = await scrapeContent(url); const data = await scrapeContent(url);
return data;
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) { } 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,42 +1,8 @@
import { scrapeContent as searchScrape } from "../../scraper/eporner/epornerSearchController"; import { scrapeContent as searchScrape } from "../../scraper/eporner/epornerSearchController";
import { scrapeContent as videoScrape } from "../../scraper/eporner/epornerGetController"; import { scrapeContent as videoScrape } from "../../scraper/eporner/epornerGetController";
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 randomEporner(req: Request, res: Response) {
/**
* @api {get} /eporner/random Get random eporner
* @apiName Get random eporner
* @apiGroup eporner
* @apiDescription Get a random eporner 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/eporner/random
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/eporner/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/eporner/random") as resp:
* print(await resp.json())
*/
// cat/all/SORT-top-weekly/
export async function randomEporner() {
try { try {
const weeklyUrl = `${c.EPORNER}/cat/all/SORT-top-weekly/`; const weeklyUrl = `${c.EPORNER}/cat/all/SORT-top-weekly/`;
const list = await searchScrape(weeklyUrl); const list = await searchScrape(weeklyUrl);
@@ -58,15 +24,9 @@ export async function randomEporner(req: Request, res: Response) {
const url = `${c.EPORNER}/${path}`; const url = `${c.EPORNER}/${path}`;
const data = await videoScrape(url); const data = await videoScrape(url);
logger.info({ return data;
path: req.path,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent"),
});
return res.json(data);
} catch (err) { } catch (err) {
res.status(400).json(maybeError(false, (err as Error).message)); const e = err as Error;
throw new Error(e.message);
} }
} }

View File

@@ -1,48 +1,14 @@
import { scrapeContent } from "../../scraper/eporner/epornerSearchController"; import { scrapeContent } from "../../scraper/eporner/epornerSearchController";
import c from "../../utils/options"; import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
export async function searchEporner(req: Request, res: Response) { export async function searchEporner({
query,
}: {
query: { key: string; page?: string };
}) {
try { try {
/** const { key } = query;
* @api {get} /eporner/search Search eporner videos const page = Number(query.page || 1);
* @apiName Search eporner
* @apiGroup eporner
* @apiDescription Search eporner videos
* @apiParam {String} key Keyword to search
* @apiParam {Number} [page=1] Page number
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/eporner/search?key=milf
* curl -i https://lust.scathach.id/eporner/search?key=milf&page=2
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/eporner/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/eporner/search?key=milf") as resp:
* print(await resp.json())
*/
// https://www.eporner.com/tag/milf/
const key = req.query.key as string;
const page = Number(req.query.page || 1);
if (!key) throw Error("Parameter key is required");
if (isNaN(page)) throw Error("Parameter page must be a number");
const slug = key const slug = key
.toLowerCase() .toLowerCase()
@@ -50,23 +16,14 @@ export async function searchEporner(req: Request, res: Response) {
.replace(/\s+/g, "-"); .replace(/\s+/g, "-");
const url = const url =
page === 1 page === 1
? `${c.EPORNER}/tag/${slug}/` ? `${c.EPORNER}/tag/${slug}/`
: `${c.EPORNER}/tag/${slug}/${page}/`; : `${c.EPORNER}/tag/${slug}/${page}/`;
const data = await scrapeContent(url); const data = await scrapeContent(url);
return data;
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) { } 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/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(req: Request, res: Response) { export async function searchPornhub({
query,
}: {
query: { key: string; page?: string; sort?: string };
}) {
try { try {
/** const { key, sort } = query;
* @api {get} /pornhub/search Search pornhub videos const page = query.page || "1";
* @apiName Search pornhub
* @apiGroup pornhub
* @apiDescription Search pornhub videos
* @apiParam {String} key Keyword to search
* @apiParam {Number} [page=1] Page number
* @apiParam {String} [sort=mr] Sort by
*
* @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/search?key=milf
* curl -i https://lust.scathach.id/pornhub/search?key=milf&page=2&sort=mr
*
* @apiExample {js} JS/TS
* 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; let url;
if (!sort) url = `${c.PORNHUB}/video/search?search=${spacer(key)}`; 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 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}`;
else url = `${c.PORNHUB}/video/search?search=${spacer(key)}&o=${sort}&page=${page}`;
console.log(url);
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,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";
import { Request, Response } from "express";
export async function relatedRedtube(req: Request, res: Response) { export async function relatedRedtube({ query }: { query: { id: string } }) {
try { try {
/** const { id } = query;
* @api {get} /redtube/get?id=:id Get redtube related videos
* @apiName Get redtube related videos
* @apiGroup redtube
* @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 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,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(req: Request, res: Response) { export async function searchRedtube({
query,
}: {
query: { key: string; page?: string };
}) {
try { try {
/** const { key } = query;
* @api {get} /redtube/search Search redtube videos const page = query.page || "1";
* @apiName Search redtube
* @apiGroup redtube
* @apiDescription Search redtube videos
* @apiParam {String} key Keyword to search
* @apiParam {Number} [page=1] Page number
*
* @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/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 url = `${c.REDTUBE}/?search=${spacer(key)}&page=${page}`;
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,10 +1,5 @@
import { Request, Response } from "express"; import { lust } from "../../LustPress";
import LustPress from "../../LustPress"; import { IVideoData, TxxxResponse } from "../../interfaces";
import { maybeError } from "../../utils/modifier";
import { logger } from "../../utils/logger";
import { IVideoData } from "../../interfaces";
const lust = new LustPress();
// Generate sharded API URL exactly like TXXX expects // Generate sharded API URL exactly like TXXX expects
function getApiUrl(videoId: string): string { function getApiUrl(videoId: string): string {
@@ -17,15 +12,13 @@ function getApiUrl(videoId: string): string {
return `https://txxx.com/api/json/video/86400/${million}/${thousand}/${id}.json`; return `https://txxx.com/api/json/video/86400/${million}/${thousand}/${id}.json`;
} }
export async function getTxxx(req: Request, res: Response) { export async function getTxxx({ query }: { query: { id: string } }) {
try { try {
const id = String(req.query.id || "").trim(); const { id } = query;
if (!id) throw new Error("Parameter id is required");
const apiUrl = getApiUrl(id); const apiUrl = getApiUrl(id);
const buffer = await lust.fetchBody(apiUrl); const buffer = await lust.fetchBody(apiUrl);
const parsed = JSON.parse(buffer.toString("utf-8")); const parsed = JSON.parse(buffer.toString("utf-8")) as TxxxResponse;
if (!parsed?.video) { if (!parsed?.video) {
throw new Error("Invalid API response"); throw new Error("Invalid API response");
@@ -34,12 +27,12 @@ export async function getTxxx(req: Request, res: Response) {
const video = parsed.video; const video = parsed.video;
const categories = Object.values(video.categories || {}).map( const categories = Object.values(video.categories || {}).map(
(c: any) => c.title, (c) => c.title
); );
const tags = Object.values(video.tags || {}).map((t: any) => t.title); const tags = Object.values(video.tags || {}).map((t) => t.title);
const models = Object.values(video.models || {}).map((m: any) => m.title); const models = Object.values(video.models || {}).map((m) => m.title);
const videoDir = video.dir || ""; const videoDir = video.dir || "";
const videoId = video.video_id; const videoId = video.video_id;
@@ -63,20 +56,14 @@ export async function getTxxx(req: Request, res: Response) {
tags: [...categories, ...tags], tags: [...categories, ...tags],
}, },
source: `https://txxx.com/videos/${videoId}/${videoDir}/`, source: `https://txxx.com/videos/${videoId}/${videoDir}/`,
assets: [embed, video.thumbsrc].filter(Boolean), assets: [embed, video.thumbsrc].filter(Boolean) as string[],
}; };
logger.info({ return response;
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent"),
});
return res.json(response);
} catch (err) { } catch (err) {
const e = err as Error; const e = err as Error;
return res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,10 +1,5 @@
import { Request, Response } from "express"; import { lust } from "../../LustPress";
import LustPress from "../../LustPress"; import { ISearchVideoData, TxxxRelatedResponse } from "../../interfaces";
import { maybeError } from "../../utils/modifier";
import { logger } from "../../utils/logger";
import { ISearchVideoData } from "../../interfaces";
const lust = new LustPress();
function getRelatedApiUrl(videoId: string, page = 1, count = 50): string { function getRelatedApiUrl(videoId: string, page = 1, count = 50): string {
const id = Number(videoId); const id = Number(videoId);
@@ -19,10 +14,14 @@ function getRelatedApiUrl(videoId: string, page = 1, count = 50): string {
); );
} }
export async function relatedTxxx(req: Request, res: Response) { export async function relatedTxxx({
query,
}: {
query: { id: string; page?: string };
}) {
try { try {
const id = String(req.query.id || "").trim(); const { id } = query;
const page = Number(req.query.page || 1); const page = Number(query.page || 1);
if (!id) throw new Error("Parameter id is required"); if (!id) throw new Error("Parameter id is required");
if (Number.isNaN(page)) throw new Error("Parameter page must be a number"); if (Number.isNaN(page)) throw new Error("Parameter page must be a number");
@@ -30,14 +29,14 @@ export async function relatedTxxx(req: Request, res: Response) {
const apiUrl = getRelatedApiUrl(id, page); const apiUrl = getRelatedApiUrl(id, page);
const buffer = await lust.fetchBody(apiUrl); const buffer = await lust.fetchBody(apiUrl);
const rawData = JSON.parse(buffer.toString("utf-8")); const rawData = JSON.parse(buffer.toString("utf-8")) as TxxxRelatedResponse;
const videos = Array.isArray(rawData.videos) ? rawData.videos : []; const videos = Array.isArray(rawData.videos) ? rawData.videos : [];
const data = videos.map((v: any) => ({ const data = videos.map((v) => ({
id: v.video_id, id: String(v.video_id),
title: v.title, title: v.title,
image: v.scr || v.thumb || null, image: v.scr || v.thumb || "",
duration: v.duration || "None", duration: v.duration || "None",
views: v.video_viewed || "0", views: v.video_viewed || "0",
rating: v.rating || "0", rating: v.rating || "0",
@@ -46,24 +45,16 @@ export async function relatedTxxx(req: Request, res: Response) {
video: `https://txxx.com/embed/${v.video_id}/`, video: `https://txxx.com/embed/${v.video_id}/`,
})); }));
logger.info({ return {
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent"),
});
return res.json({
success: true, success: true,
total_count: rawData.total_count || "0", total_count: String(rawData.total_count || "0"),
pages: rawData.pages || 1, pages: rawData.pages || 1,
page, page,
data, data,
source: `https://txxx.com/videos/${id}/`, source: `https://txxx.com/videos/${id}/`,
} as ISearchVideoData); } as unknown as ISearchVideoData;
} catch (err) { } catch (err) {
const e = err as Error; const e = err as Error;
return res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,21 +1,15 @@
import { Request, Response } from "express"; import { lust } from "../../LustPress";
import LustPress from "../../LustPress"; import { TxxxSearchResponse as TxxxResponse } from "../../interfaces";
import { maybeError } from "../../utils/modifier";
import { logger } from "../../utils/logger";
const lust = new LustPress(); export async function randomTxxx() {
export async function randomTxxx(req: Request, res: Response) {
try { try {
const apiUrl = const apiUrl =
"https://txxx.com/api/json/videos2/14400/str/most-popular/60/..1.all..day.json"; "https://txxx.com/api/json/videos2/14400/str/most-popular/60/..1.all..day.json";
const buffer = await lust.fetchBody(apiUrl); const buffer = await lust.fetchBody(apiUrl);
const rawData = JSON.parse(buffer.toString("utf-8")); const rawData = JSON.parse(buffer.toString("utf-8")) as TxxxResponse;
const videos = Array.isArray(rawData.videos) const videos = Array.isArray(rawData.videos) ? rawData.videos : [];
? rawData.videos
: [];
if (videos.length === 0) { if (videos.length === 0) {
throw new Error("No videos returned from upstream"); throw new Error("No videos returned from upstream");
@@ -39,20 +33,15 @@ export async function randomTxxx(req: Request, res: Response) {
embed: `https://txxx.com/embed/${v.video_id}/`, embed: `https://txxx.com/embed/${v.video_id}/`,
}; };
logger.info({ return {
path: req.path,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent"),
});
return res.json({
success: true, success: true,
data, data,
source: apiUrl, source: apiUrl,
}); };
} catch (err) { } catch (err) {
const e = err as Error; const e = err as Error;
return res.status(400).json(maybeError(false, e.message)); throw new Error(e.message);
} }
} }

View File

@@ -1,40 +1,27 @@
import { Request, Response } from "express"; import { lust } from "../../LustPress";
import LustPress from "../../LustPress"; import { TxxxSearchResponse } from "../../interfaces";
import { maybeError } from "../../utils/modifier";
const lust = new LustPress(); export async function searchTxxx({
query,
export async function searchTxxx(req: Request, res: Response) { }: {
query: { key: string; page?: string };
}) {
try { try {
const key = String(req.query.key || "").trim(); const { key } = query;
const page = Number(req.query.page || 1); const page = Number(query.page || 1);
if (!key) {
return res.json({
success: false,
error: "Parameter key is required",
});
}
if (Number.isNaN(page)) {
return res.json({
success: false,
error: "Parameter page must be a number",
});
}
const apiUrl = const apiUrl =
"https://txxx.com/api/videos2.php" + "https://txxx.com/api/videos2.php" +
`?params=259200/str/relevance/60/search..${page}.all..` + `?params=259200/str/relevance/60/search..${page}.all..` +
`&s=${encodeURIComponent(key)}`; `&s=${encodeURIComponent(key)}`;
// Fetch from API directly // Fetch from API directly
const buffer = await lust.fetchBody(apiUrl); const buffer = await lust.fetchBody(apiUrl);
const rawData = JSON.parse(buffer.toString("utf-8")); const rawData = JSON.parse(buffer.toString("utf-8")) as TxxxSearchResponse;
const videos = Array.isArray(rawData.videos) ? rawData.videos : []; const videos = Array.isArray(rawData.videos) ? rawData.videos : [];
const data = videos.map((v: any) => ({ const data = videos.map((v) => ({
video_id: v.video_id, video_id: v.video_id,
title: v.title, title: v.title,
dir: v.dir, dir: v.dir,
@@ -49,15 +36,17 @@ export async function searchTxxx(req: Request, res: Response) {
embed: `https://txxx.com/embed/${v.video_id}/`, embed: `https://txxx.com/embed/${v.video_id}/`,
})); }));
return res.json({ return {
success: true, success: true,
total_count: String(rawData.total_count ?? videos.length), total_count: String(rawData.total_count ?? videos.length),
pages: Number(rawData.pages ?? 1), pages: Number(rawData.pages ?? 1),
page, page,
data, data,
}); };
} catch (err) { } catch (err) {
const e = err as Error; const e = err as Error;
return res.json(maybeError(false, e.message)); 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");
/**
* @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}/videos/${id}`; const url = `${c.XHAMSTER}/videos/${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,42 +1,10 @@
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 video
* @apiName Get random xhamster
* @apiGroup xhamster
* @apiDescription Get a random xhamster video from the list of newest videos.
*
* @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);
@@ -54,17 +22,10 @@ export async function randomXhamster(req: Request, res: Response) {
const data = await scrapeContent(randomUrl); const data = await scrapeContent(randomUrl);
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/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(req: Request, res: Response) { export async function searchXhamster({
query,
}: {
query: { key: string; page?: string };
}) {
try { try {
/** const { key } = query;
* @api {get} /xhamster/search Search xhamster videos const page = query.page || "1";
* @apiName Search xhamster
* @apiGroup xhamster
* @apiDescription Search xhamster videos
* @apiParam {String} key Keyword to search
* @apiParam {Number} [page=1] Page number
*
* @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/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 url = `${c.XHAMSTER}/search/${spacer(key)}?page=${page}`;
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/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(req: Request, res: Response) { export async function searchXnxx({
query,
}: {
query: { key: string; page?: string };
}) {
try { try {
/** const { key } = query;
* @api {get} /xnxx/search Search xnxx videos const page = query.page || "0";
* @apiName Search xnxx
* @apiGroup xnxx
* @apiDescription Search xnxx videos
* @apiParam {String} key Keyword to search
* @apiParam {Number} [page=0] Page number
*
* @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/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 url = `${c.XNXX}/search/${spacer(key)}/${page}`;
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/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")
@@ -46,18 +15,12 @@ export async function randomXvideos(req: Request, res: Response) {
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(req: Request, res: Response) { export async function searchXvideos({
query,
}: {
query: { key: string; page?: string };
}) {
try { try {
/** const { key } = query;
* @api {get} /xvideos/search Search xvideos videos const page = query.page || "0";
* @apiName Search xvideos
* @apiGroup xvideos
* @apiDescription Search xvideos videos
* @apiParam {String} key Keyword to search
* @apiParam {Number} [page=0] Page number
*
* @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/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 url = `${c.XVIDEOS}/?k=${spacer(key)}&p=${page}`;
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/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(req: Request, res: Response) { export async function searchYouporn({
query,
}: {
query: { key: string; page?: string };
}) {
try { try {
/** const { key } = query;
* @api {get} /youporn/search Search youporn videos const page = query.page || "1";
* @apiName Search youporn
* @apiGroup youporn
* @apiDescription Search youporn videos
* @apiParam {String} key Keyword to search
* @apiParam {Number} [page=1] Page number
*
* @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/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 url = `${c.YOUPORN}/search/?query=${spacer(key)}&page=${page}`;
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,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 lust = new LustPress(); const app = new Elysia()
const app = express(); .use(cors())
.use(
swagger({
app.get("/", slow, limiter, async (req, res) => { path: "/docs",
res.send({ documentation: {
info: {
title: "Lustpress API",
version: pkg.version,
description: pkg.description,
},
},
})
)
.get("/", async () => ({
success: true, success: true,
playground: "https://sinkaroid.github.io/lustpress", playground: "https://sinkaroid.github.io/lustpress",
endpoint: "https://github.com/sinkaroid/lustpress/blob/master/README.md#routing", endpoint:
"https://github.com/sinkaroid/lustpress/blob/master/README.md#routing",
date: new Date().toLocaleString(), date: new Date().toLocaleString(),
rss: lust.currentProccess().rss, rss: lust.currentProccess().rss,
heap: lust.currentProccess().heap, heap: lust.currentProccess().heap,
server: await lust.getServer(), server: await lust.getServer(),
version: `${pkg.version}`, version: pkg.version,
}); }))
logger.info({ .use(scrapeRoutes)
path: req.path, .onError(({ code, error, set }) => {
method: req.method, console.log("Error occurred:", error);
ip: req.ip, if (code === "NOT_FOUND") {
useragent: req.get("User-Agent") set.status = 404;
}); return {
}); success: false,
message: (error as Error).message || "Not Found",
};
}
set.status = 500;
return {
success: false,
message: (error as Error).message || "Internal Server Error",
stack: (error as Error).stack,
};
})
.listen(process.env.PORT || 3000);
app.use(scrapeRoutes()); console.log(
app.use((req: Request, res: Response, next: NextFunction) => { `Lustpress is running at ${app.server?.hostname}:${app.server?.port}`
res.status(404); );
next(Error(`The page not found in path ${req.url} and method ${req.method}`));
logger.error({
path: req.url,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
});
app.use((error: any, res: Response) => { export type App = typeof app;
res.status(500).json({
message: error.message,
stack: error.stack
});
});
app.listen(process.env.PORT || 3000, () => console.log(`${pkg.name} is running on port ${process.env.PORT || 3000}`));

View File

@@ -1,27 +1,28 @@
// 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;
channel?: string; channel?: string;
models: string[]; models: string[];
tags: string[]; tags: string[];
}; };
source: string; source: string;
assets: string[]; assets: string[];
} }
export interface ISearchVideoData { export interface ISearchVideoData {
success: boolean; success: boolean;
data: string[]; data: string[];
source: string; source: string;
} }
export interface ISearchItem { export interface ISearchItem {
@@ -36,8 +37,159 @@ export interface ISearchItem {
video?: 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,7 +1,4 @@
import cors from "cors"; import { Elysia, t } from "elysia";
import { Router } from "express";
import { slow, limiter } from "../utils/limit-options";
// EPorner // EPorner
import { getEporner } from "../controller/eporner/epornerGet"; import { getEporner } from "../controller/eporner/epornerGet";
@@ -51,43 +48,160 @@ import { searchYouporn } from "../controller/youporn/youpornSearch";
import { relatedYouporn } from "../controller/youporn/youpornGetRelated"; import { relatedYouporn } from "../controller/youporn/youpornGetRelated";
import { randomYouporn } from "../controller/youporn/youpornRandom"; import { randomYouporn } from "../controller/youporn/youpornRandom";
function scrapeRoutes() { const queryId = {
const router = Router(); query: t.Object({ id: t.String() }),
};
router.get("/pornhub/get", cors(), slow, limiter, getPornhub); const querySearch = {
router.get("/pornhub/search", cors(), slow, limiter, searchPornhub); query: t.Object({
router.get("/pornhub/random", cors(), slow, limiter, randomPornhub); key: t.String(),
router.get("/pornhub/related", cors(), slow, limiter, relatedPornhub); page: t.Optional(t.String()),
router.get("/xnxx/get", cors(), slow, limiter, getXnxx); }),
router.get("/xnxx/search", cors(), slow, limiter, searchXnxx); };
router.get("/xnxx/related", cors(), slow, limiter, relatedXnxx);
router.get("/xnxx/random", cors(), slow, limiter, randomXnxx);
router.get("/redtube/get", cors(), slow, limiter, getRedtube);
router.get("/redtube/search", cors(), slow, limiter, searchRedtube);
router.get("/redtube/related", cors(), slow, limiter, relatedRedtube);
router.get("/redtube/random", cors(), slow, limiter, randomRedtube);
router.get("/xvideos/get", cors(), slow, limiter, getXvideos);
router.get("/xvideos/search", cors(), slow, limiter, searchXvideos);
router.get("/xvideos/random", cors(), slow, limiter, randomXvideos);
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);
router.get("/xhamster/related", cors(), slow, limiter, relatedXhamster);
router.get("/youporn/get", cors(), slow, limiter, getYouporn);
router.get("/youporn/search", cors(), slow, limiter, searchYouporn);
router.get("/youporn/related", cors(), slow, limiter, relatedYouporn);
router.get("/youporn/random", cors(), slow, limiter, randomYouporn);
router.get("/eporner/get", cors(), slow, limiter, getEporner);
router.get("/eporner/search", cors(), slow, limiter, searchEporner);
router.get("/eporner/related", cors(), slow, limiter, relatedEporner);
router.get("/eporner/random", cors(), slow, limiter, randomEporner);
router.get("/txxx/get", cors(), slow, limiter, getTxxx);
router.get("/txxx/search", cors(), slow, limiter, searchTxxx);
router.get("/txxx/related", cors(), slow, limiter, relatedTxxx);
router.get("/txxx/random", cors(), slow, limiter, randomTxxx);
return router; export const scrapeRoutes = (app: Elysia) =>
} app
.group("/pornhub", (app) =>
export default scrapeRoutes; app
.get("/get", getPornhub, {
...queryId,
detail: { summary: "Get Pornhub video data", tags: ["Pornhub"] },
})
.get("/search", searchPornhub, {
...querySearch,
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

@@ -1,9 +1,7 @@
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();
function calculateRatingFromStrings( function calculateRatingFromStrings(
upVote: string, upVote: string,
downVote: string downVote: string

View File

@@ -1,9 +1,7 @@
import { load } from "cheerio"; import { load } from "cheerio";
import LustPress from "../../LustPress"; import { lust } from "../../LustPress";
import { ISearchVideoData } from "../../interfaces"; import { ISearchVideoData } from "../../interfaces";
const lust = new LustPress();
export async function scrapeContent(url: string) { export async function scrapeContent(url: string) {
const html = await lust.fetchBody(url); const html = await lust.fetchBody(url);
const $ = load(html); const $ = load(html);

View File

@@ -1,9 +1,7 @@
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);

View File

@@ -1,9 +1,7 @@
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) { export async function scrapeContent(url: string) {
try { try {
@@ -11,17 +9,17 @@ export async function scrapeContent(url: string) {
const $ = load(res); const $ = load(res);
class PornhubSearch { class PornhubSearch {
search: object[]; search: PornhubSearchItem[];
data: object; data: PornhubSearchItem[];
constructor() { constructor() {
this.search = $("div.wrap") this.search = $("div.wrap")
.map((i, el) => { .map((i, el) => {
const link = $(el).find("a").attr("href"); const link = $(el).find("a").attr("href") || "";
const id = link?.split("=")[1]; const id = link.split("=")[1] || "None";
const title = $(el).find("a").attr("title"); const title = $(el).find("a").attr("title") || "None";
const image = $(el).find("img").attr("src"); const image = $(el).find("img").attr("src") || "None";
const duration = $(el).find("var.duration").text(); const duration = $(el).find("var.duration").text() || "None";
const views = $(el).find("div.videoDetailsBlock").find("span.views").text(); const views = $(el).find("div.videoDetailsBlock").find("span.views").text() || "None";
return { return {
link: `${c.PORNHUB}${link}`, link: `${c.PORNHUB}${link}`,
id: id, id: id,
@@ -33,7 +31,7 @@ export async function scrapeContent(url: string) {
}; };
}).get(); }).get();
this.data = this.search.filter((el: any) => { this.data = this.search.filter((el) => {
return el.link.includes("javascript:void(0)") === false && el.image?.startsWith("data:image") === false; return el.link.includes("javascript:void(0)") === false && el.image?.startsWith("data:image") === false;
}); });
} }
@@ -42,16 +40,13 @@ export async function scrapeContent(url: string) {
const ph = new PornhubSearch(); const ph = new PornhubSearch();
if (ph.search.length === 0) throw Error("No result found"); if (ph.search.length === 0) throw Error("No result found");
const data = ph.data as string[];
const result: ISearchVideoData = { const result: ISearchVideoData = {
success: true, success: true,
data: data, data: ph.data as unknown as 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,9 +1,7 @@
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) { export async function scrapeContent(url: string) {
try { try {
@@ -12,20 +10,19 @@ export async function scrapeContent(url: string) {
class RedTubeSearch { class RedTubeSearch {
views: string[]; views: string[];
search: object[]; search: RedTubeSearchItem[];
data: object; data: RedTubeSearchItem[];
constructor() { constructor() {
this.views = $("span.video_count") this.views = $("span.video_count")
.map((i, el) => { .map((i, el) => {
const views = $(el).text(); return $(el).text();
return views;
}).get(); }).get();
this.search = $("a.video_link") this.search = $("a.video_link")
.map((i, el) => { .map((i, el) => {
const link = $(el).attr("href"); const link = $(el).attr("href") || "";
const id = link?.split("/")[1]; const id = link.split("/")[1] || "None";
const title = $(el).find("img").attr("alt"); const title = $(el).find("img").attr("alt") || "None";
const image = $(el).find("img").attr("data-src"); const image = $(el).find("img").attr("data-src") || "None";
const duration = $(el).find("span.duration").text().split(" ").map((el: string) => { const duration = $(el).find("span.duration").text().split(" ").map((el: string) => {
return el.replace(/[^0-9:]/g, ""); return el.replace(/[^0-9:]/g, "");
}).filter((el: string) => { }).filter((el: string) => {
@@ -38,29 +35,23 @@ export async function scrapeContent(url: string) {
title: title, title: title,
image: image, image: image,
duration: duration, duration: duration,
views: this.views[i], views: this.views[i] || "None",
video: `https://embed.redtube.com/?id=${id}`, video: `https://embed.redtube.com/?id=${id}`,
}; };
}).get(); }).get();
this.data = this.search.filter((el) => {
this.data = this.search.filter((el: any) => {
return el.link.includes("javascript:void(0)") === false && el.image?.startsWith("data:image") === false; return el.link.includes("javascript:void(0)") === false && el.image?.startsWith("data:image") === false;
}); });
} }
} }
const red = new RedTubeSearch(); const red = new RedTubeSearch();
if (red.search.length === 0) throw Error("No result found"); if (red.search.length === 0) throw Error("No result found");
const data = red.data as string[];
const result: ISearchVideoData = { const result: ISearchVideoData = {
success: true, success: true,
data: data, data: red.data as unknown as string[],
source: url, source: url,
}; };
return result; return result;

View File

@@ -1,8 +1,6 @@
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 {
@@ -10,11 +8,11 @@ export async function scrapeContent(url: string) {
const $ = load(buffer.toString("utf8")); const $ = load(buffer.toString("utf8"));
const raw = $("#initials-script").html(); const raw = $("#initials-script").html();
const initials = raw const initials = (raw
? JSON.parse( ? JSON.parse(
raw.replace(/^window\.initials\s*=\s*/, "").replace(/;$/, ""), raw.replace(/^window\.initials\s*=\s*/, "").replace(/;$/, ""),
) )
: null; : null) as XhamsterInitials | null;
class Xhamster { class Xhamster {
link: string; link: string;
@@ -71,13 +69,13 @@ export async function scrapeContent(url: string) {
this.tags = this.tags =
initials?.videoTagsComponent?.tags initials?.videoTagsComponent?.tags
?.filter((t: any) => t.isTag) ?.filter((t) => t.isTag)
.map((t: any) => t.name) || []; .map((t) => t.name) || [];
this.models = this.models =
initials?.videoTagsComponent?.tags initials?.videoTagsComponent?.tags
?.filter((t: any) => t.isPornstar) ?.filter((t) => t.isPornstar)
.map((t: any) => t.name) || []; .map((t) => t.name) || [];
const embedId = this.link.split("-").pop()?.replace("/", ""); const embedId = this.link.split("-").pop()?.replace("/", "");
this.video = embedId ? `https://xhamster.com/embed/${embedId}` : "None"; this.video = embedId ? `https://xhamster.com/embed/${embedId}` : "None";
@@ -109,3 +107,4 @@ export async function scrapeContent(url: string) {
throw new Error(e.message); throw new Error(e.message);
} }
} }

View File

@@ -1,9 +1,7 @@
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, XhamsterSearchItem } from "../../interfaces";
const lust = new LustPress();
export async function scrapeContent(url: string) { export async function scrapeContent(url: string) {
try { try {
@@ -11,32 +9,30 @@ export async function scrapeContent(url: string) {
const $ = load(res); const $ = load(res);
class XhamsterSearch { class XhamsterSearch {
search: any; search: XhamsterSearchItem[];
constructor() { constructor() {
const views = $("div.video-thumb-views") const views = $("div.video-thumb-views")
.map((i, el) => { .map((i, el) => {
const views = $(el).text(); return $(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) => {
const duration = $(el).text(); return $(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], id: link.split("/")[4] || "None",
title: $(el).find("img").attr("alt"), title: $(el).find("img").attr("alt") || "None",
image: $(el).find("img").attr("src"), image: $(el).find("img").attr("src") || "None",
duration: duration[i], duration: duration[i] || "None",
views: views[i], views: views[i] || "None",
video: `${c.XHAMSTER}/embed/${link?.split("-").pop()}`, video: `${c.XHAMSTER}/embed/${link.split("-").pop()}`,
}; };
}) })
.get(); .get();
@@ -45,10 +41,9 @@ export async function scrapeContent(url: string) {
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: data, data: xh.search as unknown as string[],
source: url, source: url,
}; };
return result; return result;

View File

@@ -1,37 +1,31 @@
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) { 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 PornhubSearch { class XnxxSearch {
search: object[]; search: object[];
data: object;
constructor() { constructor() {
// in <div id="video-player-bg"> get <script>var video_related=
this.search = $("div#video-player-bg") this.search = $("div#video-player-bg")
.map((i, el) => { .map((i, el) => {
const script = $(el).find("script").html(); const script = $(el).find("script").html();
const video_related = script?.split("var video_related=")[1]; const video_related = script?.split("var video_related=")[1];
//stop and replace everything after the last ];
const badJson = video_related?.split("];")[0] + "]"; const badJson = video_related?.split("];")[0] + "]";
const actualResult = JSON.parse(String(badJson)); const actualResult = JSON.parse(String(badJson)) as XnxxRelatedRaw[];
//console.log(actualResult); const result = actualResult.map((rel) => {
const result = actualResult.map((el: any) => {
return { return {
link: `${c.XNXX}${el.u}`, link: `${c.XNXX}${rel.u}`,
id: el.u.slice(1, -1), id: rel.u.slice(1, -1),
title: el.t, title: rel.t,
image: el.i, image: rel.i,
duration: el.d, duration: rel.d,
views: `${el.n}, ${el.r}`, views: `${rel.n}, ${rel.r}`,
video: `${c.XNXX}/embedframe/${el.id}` video: `${c.XNXX}/embedframe/${rel.id}`
}; };
}); });
return result; return result;
@@ -39,12 +33,11 @@ export async function scrapeContent(url: string) {
} }
} }
const x = new PornhubSearch(); const x = new XnxxSearch();
if (x.search.length === 0) throw Error("No result found"); if (x.search.length === 0) throw Error("No result found");
const data = x.search as unknown as string[];
const result: ISearchVideoData = { const result: ISearchVideoData = {
success: true, success: true,
data: data, data: x.search as unknown as string[],
source: url, source: url,
}; };
return result; return result;

View File

@@ -1,9 +1,7 @@
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) { export async function scrapeContent(url: string) {
try { try {
@@ -12,23 +10,22 @@ export async function scrapeContent(url: string) {
class XvideosSearch { class XvideosSearch {
search: object[]; search: object[];
data: object;
constructor() { constructor() {
this.search = $("div#video-player-bg") this.search = $("div#video-player-bg")
.map((i, el) => { .map((i, el) => {
const script = $(el).find("script").html(); const script = $(el).find("script").html();
const video_related = script?.split("var video_related=")[1]; const video_related = script?.split("var video_related=")[1];
const badJson = video_related?.split("];")[0] + "]"; const badJson = video_related?.split("];")[0] + "]";
const actualResult = JSON.parse(String(badJson)); const actualResult = JSON.parse(String(badJson)) as XvideosRelatedRaw[];
const result = actualResult.map((el: any) => { const result = actualResult.map((rel) => {
return { return {
link: `${c.XVIDEOS}${el.u}`, link: `${c.XVIDEOS}${rel.u}`,
id: el.u.slice(1, -1), id: rel.u.slice(1, -1),
title: el.t, title: rel.t,
image: el.i, image: rel.i,
duration: el.d, duration: rel.d,
views: `${el.n}, ${el.r}`, views: `${rel.n}, ${rel.r}`,
video: `${c.XVIDEOS}/embedframe/${el.id}` video: `${c.XVIDEOS}/embedframe/${rel.id}`
}; };
}); });
return result; return result;
@@ -38,10 +35,9 @@ export async function scrapeContent(url: string) {
const x = new XvideosSearch(); const x = new XvideosSearch();
if (x.search.length === 0) throw Error("No result found"); if (x.search.length === 0) throw Error("No result found");
const data = x.search as unknown as string[];
const result: ISearchVideoData = { const result: ISearchVideoData = {
success: true, success: true,
data: data, data: x.search as unknown as string[],
source: url, source: url,
}; };
return result; return result;

View File

@@ -1,9 +1,7 @@
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) { export async function scrapeContent(url: string) {
try { try {
@@ -11,7 +9,7 @@ export async function scrapeContent(url: string) {
const $ = load(res); const $ = load(res);
class XvideosSearch { class XvideosSearch {
search: object[]; search: XvideosSearchItem[];
constructor() { constructor() {
const data = $("div.thumb-under") const data = $("div.thumb-under")
.map((i, el) => { .map((i, el) => {
@@ -26,23 +24,25 @@ export async function scrapeContent(url: string) {
this.search = $("div.mozaique.cust-nb-cols") this.search = $("div.mozaique.cust-nb-cols")
.find("div.thumb") .find("div.thumb")
.map((i, el) => { .map((i, el) => {
const href = $(el).find("a").attr("href") || "None";
const videoId = $(el).find("img").attr("data-videoid");
return { return {
link: `${c.XVIDEOS}${$(el).find("a").attr("href")}` || "None", link: `${c.XVIDEOS}${href}`,
id: $(el).find("a").attr("href") || "None", id: href,
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/${$(el).find("img").attr("data-videoid")}` video: `${c.XVIDEOS}/embedframe/${videoId}`
}; };
}).get(); }).get();
this.search = this.search.filter((el: any) => { this.search = this.search.filter((el) => {
return !el.id.includes("THUMBNUM"); return !el.id.includes("THUMBNUM");
}); });
this.search = this.search.filter((el: any) => { this.search = this.search.filter((el) => {
return el.id.includes("/video"); return el.id.includes("/video");
}); });
} }
@@ -50,10 +50,9 @@ export async function scrapeContent(url: string) {
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 data = xv.search as unknown as string[];
const result: ISearchVideoData = { const result: ISearchVideoData = {
success: true, success: true,
data: data, data: xv.search as unknown as string[],
source: url, source: url,
}; };
return result; return result;

View File

@@ -1,10 +1,8 @@
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);

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

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;
}

View File

@@ -1,6 +1,5 @@
import test from "node:test"; import { test } from "bun:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import p from "phin";
const port = process.env.PORT ?? 3000; const port = process.env.PORT ?? 3000;
@@ -12,15 +11,11 @@ type ApiResponse = {
}; };
async function run(path: string) { async function run(path: string) {
const res = await p({ const res = await fetch(`http://localhost:${port}${path}`);
url: `http://localhost:${port}${path}`,
parse: "json",
timeout: 20000
});
assert.equal(res.statusCode, 200); assert.equal(res.status, 200);
const json = res.body as ApiResponse; const json = (await res.json()) as ApiResponse;
console.log(JSON.stringify(json, null, 2)); console.log(JSON.stringify(json, null, 2));
@@ -31,32 +26,32 @@ async function run(path: string) {
test("pornhub", async () => { test("pornhub", async () => {
await run("/pornhub/random"); await run("/pornhub/random");
}); }, 300000);
test("xnxx", async () => { test("xnxx", async () => {
await run("/xnxx/random"); await run("/xnxx/random");
}); }, 120000);
test("redtube", async () => { test("redtube", async () => {
await run("/redtube/random"); await run("/redtube/random");
}); }, 120000);
test("xvideos", async () => { test("xvideos", async () => {
await run("/xvideos/random"); await run("/xvideos/random");
}); }, 120000);
test("xhamster", async () => { test("xhamster", async () => {
await run("/xhamster/random"); await run("/xhamster/random");
}); }, 120000);
test("youporn", async () => { test("youporn", async () => {
await run("/youporn/random"); await run("/youporn/random");
}); }, 120000);
test("eporner", async () => { test("eporner", async () => {
await run("/eporner/random"); await run("/eporner/random");
}); }, 120000);
test("txxx", async () => { test("txxx", async () => {
await run("/txxx/random"); await run("/txxx/random");
}); }, 120000);

View File

@@ -1,25 +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",
"skipLibCheck": true "allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowUnreachableCode": false,
"baseUrl": "src",
"paths": {}
}, },
"include": [ "include": [
"src/**/*" "src/**/*"