Compare commits
9 Commits
indev
...
8.1.2-alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e63195035d | ||
|
|
09b49f1649 | ||
|
|
ac1a6d82fb | ||
|
|
2b682791d2 | ||
|
|
d2765c8e34 | ||
|
|
55e88b57d1 | ||
|
|
5f641096a2 | ||
|
|
fcf2cff58b | ||
|
|
0f88b3ce5b |
@@ -11,4 +11,4 @@ REDIS_URL = redis://default:somenicepassword@redis-666.c10.us-east-6-6.ec666.clo
|
|||||||
EXPIRE_CACHE = 1
|
EXPIRE_CACHE = 1
|
||||||
|
|
||||||
# you must identify your origin, if not set it will use default
|
# you must identify your origin, if not set it will use default
|
||||||
USER_AGENT = "lustpress/1.6.0 Node.js/16.9.1"
|
USER_AGENT = "lustpress/8.0.1 Node.js/22.22.0"
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
build/
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"es2021": true,
|
|
||||||
"node": true,
|
|
||||||
"commonjs": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 12,
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"@typescript-eslint"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"linebreak-style": 0,
|
|
||||||
"quotes": [
|
|
||||||
"error",
|
|
||||||
"double"
|
|
||||||
],
|
|
||||||
"semi": [
|
|
||||||
"error",
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"no-empty": "error",
|
|
||||||
"no-func-assign": "error",
|
|
||||||
"no-case-declarations": "off",
|
|
||||||
"no-unreachable": "error",
|
|
||||||
"no-eval": "error",
|
|
||||||
"no-global-assign": "error",
|
|
||||||
"@typescript-eslint/no-explicit-any": ["off"],
|
|
||||||
"indent": [
|
|
||||||
"error",
|
|
||||||
2
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
.github/workflows/build.yml
vendored
24
.github/workflows/build.yml
vendored
@@ -6,26 +6,28 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
permissions:
|
||||||
build:
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
- name: Check lint
|
- name: Check lint
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
43
.github/workflows/docker.yml
vendored
43
.github/workflows/docker.yml
vendored
@@ -6,22 +6,35 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Log into GitHub Container Registry
|
|
||||||
run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
|
||||||
|
|
||||||
- name: Get the package.json version
|
|
||||||
id: package-version
|
|
||||||
run: echo ::set-output name=version::$(jq -r .version package.json)
|
|
||||||
- name: Build the Docker image
|
|
||||||
run: docker build . --file Dockerfile --tag ghcr.io/${{ github.repository }}:${{ steps.package-version.outputs.version }}
|
|
||||||
- name: Tag the Docker image
|
|
||||||
run: docker tag ghcr.io/${{ github.repository }}:${{ steps.package-version.outputs.version }} ghcr.io/${{ github.repository }}:latest
|
|
||||||
- name: Push the Docker image
|
|
||||||
run: docker push ghcr.io/${{ github.repository }} --all-tags
|
|
||||||
|
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
|
|
||||||
|
- name: Set image name
|
||||||
|
run: echo "IMAGE_NAME=ghcr.io/$GITHUB_REPOSITORY" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Log into GitHub Container Registry
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
|
||||||
|
|
||||||
|
- name: Get the package.json version
|
||||||
|
run: |
|
||||||
|
VERSION=$(jq -r .version package.json)
|
||||||
|
echo "IMAGE_TAG=$VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build the Docker image
|
||||||
|
run: docker build . --file Dockerfile --tag "$IMAGE_NAME:$IMAGE_TAG"
|
||||||
|
|
||||||
|
- name: Tag the Docker image
|
||||||
|
run: docker tag "$IMAGE_NAME:$IMAGE_TAG" "$IMAGE_NAME:latest"
|
||||||
|
|
||||||
|
- name: Push the Docker image
|
||||||
|
run: docker push "$IMAGE_NAME" --all-tags
|
||||||
36
.github/workflows/eporner.yml
vendored
Normal file
36
.github/workflows/eporner.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Eporner test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Eporner test
|
||||||
|
run: npm run test:eporner
|
||||||
21
.github/workflows/playground.yml
vendored
21
.github/workflows/playground.yml
vendored
@@ -6,21 +6,32 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
concurrency: ci-${{ github.ref }}
|
concurrency: ci-${{ github.ref }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Installing dependencies
|
steps:
|
||||||
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
- name: Generating docs
|
- name: Generating docs
|
||||||
run: npm run build:apidoc
|
run: npm run build:apidoc
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
uses: JamesIves/github-pages-deploy-action@9d877eea73427180ae43cf98e8914934fe157a1a
|
||||||
with:
|
with:
|
||||||
branch: gh-pages
|
branch: gh-pages
|
||||||
folder: docs
|
folder: docs
|
||||||
26
.github/workflows/pornhub.yml
vendored
26
.github/workflows/pornhub.yml
vendored
@@ -6,23 +6,29 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
permissions:
|
||||||
build:
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
|||||||
26
.github/workflows/redtube.yml
vendored
26
.github/workflows/redtube.yml
vendored
@@ -6,23 +6,29 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
permissions:
|
||||||
build:
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
|||||||
24
.github/workflows/status.yml
vendored
24
.github/workflows/status.yml
vendored
@@ -6,24 +6,28 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
permissions:
|
||||||
build:
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Check status code
|
- name: Check status code
|
||||||
run: npm run test
|
run: npm run test
|
||||||
36
.github/workflows/txxx.yml
vendored
Normal file
36
.github/workflows/txxx.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Txxx test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Txxx test
|
||||||
|
run: npm run test:txxx
|
||||||
26
.github/workflows/xhamster.yml
vendored
26
.github/workflows/xhamster.yml
vendored
@@ -6,23 +6,29 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
permissions:
|
||||||
build:
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
|||||||
26
.github/workflows/xnxx.yml
vendored
26
.github/workflows/xnxx.yml
vendored
@@ -6,23 +6,29 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
permissions:
|
||||||
build:
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
|||||||
26
.github/workflows/xvideos.yml
vendored
26
.github/workflows/xvideos.yml
vendored
@@ -6,23 +6,29 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
permissions:
|
||||||
build:
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
|||||||
26
.github/workflows/youporn.yml
vendored
26
.github/workflows/youporn.yml
vendored
@@ -6,23 +6,29 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
permissions:
|
||||||
build:
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
# Closing remarks
|
# Closing remarks
|
||||||
|
|
||||||
I hope you have found this project useful. All the major credit really goes to the all the doujin sites, which
|
I hope you have found this project useful. All the major credit really goes to the all the r18 sites, which
|
||||||
allows this services to operate.
|
allows this services to operate.
|
||||||
|
|
||||||
- [PornHub](https://pornhub.com)
|
|
||||||
- [Xnxx](https://xnxx.com)
|
|
||||||
- [RedTube](https://redtube.com)
|
|
||||||
- [Xvideos](https://xvideos.com)
|
|
||||||
- [Xhamster](https://xhamster.com)
|
|
||||||
- [YouPorn](https://youporn.com)
|
|
||||||
- [JavHD](https://javhd.com)
|
|
||||||
|
|
||||||
Core dependencies:
|
Core dependencies:
|
||||||
- [express](https://github.com/expressjs/express)
|
- [express](https://github.com/expressjs/express)
|
||||||
- [cheerio](https://cheerio.js.org/)
|
- [cheerio](https://cheerio.js.org/)
|
||||||
- [keyv](https://github.com/jaredwray/keyv)
|
- [keyv](https://github.com/jaredwray/keyv)
|
||||||
|
|
||||||
# Alternative-links
|
## Frequently asked questions
|
||||||
Just in case if https://lust.scathach.id down, here some alternative deployment
|
**Q: The website response is slow**
|
||||||
|
> That's unfortunate, this repository was opensource already, You can host and deploy Lustpress with your own instance. Any fixes and improvements will updating to this repo.
|
||||||
|
|
||||||
- TBA
|
> **March 11, 2026**:
|
||||||
|
We have discontinued providing public APIs and playground services due to ongoing abuse and excessive usage.
|
||||||
|
To continue using Lustpress, please deploy and run your own self-hosted instance.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:latest
|
FROM node:22
|
||||||
|
|
||||||
WORKDIR /srv/app
|
WORKDIR /srv/app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|||||||
178
README.md
178
README.md
@@ -1,14 +1,14 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://lust.scathach.id"><img width="500" src="https://cdn.discordapp.com/attachments/1046495201176334467/1097683154959077496/lust-node_1.png" alt="lustpress"></a>
|
<a href="http://localhost:3000/"><img width="500" src="resources/project/images/lustpress-node_1.png" alt="lustpress"></a>
|
||||||
|
|
||||||
<h4 align="center">RESTful and experimental API for the PornHub and other adult videos</h4>
|
<h4 align="center">RESTful and experimental API for PornHub and other R18 websites</h4>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/sinkaroid/lustpress/actions/workflows/playground.yml"><img src="https://github.com/sinkaroid/lustpress/workflows/Playground/badge.svg"></a>
|
<a href="https://github.com/sinkaroid/lustpress/actions/workflows/playground.yml"><img src="https://github.com/sinkaroid/lustpress/workflows/Playground/badge.svg"></a>
|
||||||
<a href="https://codeclimate.com/github/sinkaroid/lustpress/maintainability"><img src="https://api.codeclimate.com/v1/badges/29a2be78f853f9e3a4a3/maintainability" /></a>
|
<a href="https://codeclimate.com/github/sinkaroid/lustpress/maintainability"><img src="https://api.codeclimate.com/v1/badges/29a2be78f853f9e3a4a3/maintainability" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Lustpress stand for Lust and **Express**, rebuild from [Jandapress](https://github.com/sinkaroid/jandapress) with completely different approach.
|
Lustpress stand for Lust and **Express**, rebuild from [Jandapress](https://github.com/sinkaroid/jandapress) with completely different approach.
|
||||||
The motivation of this project is to bring you an actionable data related to pornhub and other adult videos with gather, similar design pattern, endpoint bindings and consistent structure in mind.
|
The motivation of this project is to bring you an actionable data related to pornhub and other r18 sites with gather, similar design pattern, endpoint bindings, and consistent structure in mind.
|
||||||
|
|
||||||
<a href="https://sinkaroid.github.io/lustpress">Playground</a> •
|
<a href="https://sinkaroid.github.io/lustpress">Playground</a> •
|
||||||
<a href="https://github.com/sinkaroid/lustpress/blob/master/CONTRIBUTING.md">Contributing</a> •
|
<a href="https://github.com/sinkaroid/lustpress/blob/master/CONTRIBUTING.md">Contributing</a> •
|
||||||
@@ -17,49 +17,57 @@ The motivation of this project is to bring you an actionable data related to por
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<a href="https://lust.scathach.id"><img align="right" src="https://cdn.discordapp.com/attachments/1046495201176334467/1097784363963383848/lp_copy.png" width="320"></a>
|
<a href="http://localhost:3000/"><img align="right" src="resources/project/images/bnnuy.png" width="320"></a>
|
||||||
|
|
||||||
- [Lustpress](#)
|
- [Lustpress](#)
|
||||||
- [The problem](#the-problem)
|
- [The problem](#the-problem)
|
||||||
- [The solution](#the-solution)
|
- [The solution](#the-solution)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Lustpress avaibility](#lustpress-avaibility)
|
- [Running tests](#Running-tests)
|
||||||
- [Prerequisites](#prerequisites)
|
- [Prerequisites](#prerequisites)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Docker](#docker)
|
- [Docker](#docker)
|
||||||
- [Manual](#manual)
|
- [Manual](#manual)
|
||||||
- [Running tests](#running-tests)
|
- [Tests case](#tests)
|
||||||
- [Playground](https://sinkaroid.github.io/lustpress)
|
- [Playground](https://sinkaroid.github.io/lustpress)
|
||||||
- [Routing](#playground)
|
- [Routing](#playground)
|
||||||
- [Status response](#status-response)
|
- [Status response](#status-response)
|
||||||
- [CLosing remarks](https://github.com/sinkaroid/lustpress/blob/master/CLOSING_REMARKS.md)
|
- [CLosing remarks](https://github.com/sinkaroid/lustpress/blob/master/CLOSING_REMARKS.md)
|
||||||
- [Alternative links](https://github.com/sinkaroid/lustpress/blob/master/CLOSING_REMARKS.md#alternative-links)
|
- [Alternative links](https://github.com/sinkaroid/lustpress/blob/master/CLOSING_REMARKS.md#alternative-links)
|
||||||
- [Pronunciation](#Pronunciation)
|
- [Pronunciation](#Pronunciation)
|
||||||
- [Client libraries / Wrappers](#client-libraries--wrappers)
|
- [Client libraries](#client-libraries)
|
||||||
- [Acknowledgements](#acknowledgements)
|
- [Acknowledgements](#acknowledgements)
|
||||||
- [Legal](#legal)
|
- [Legal](#legal)
|
||||||
|
- [Discontinued playground](#frequently-asked-questions)
|
||||||
|
- [again, discontinued playground](#frequently-asked-questions)
|
||||||
|
|
||||||
|
|
||||||
## The problem
|
## The problem
|
||||||
Even official API is exists, they were lack and bad behavior, every single site has own structure and different way to interacts with.
|
|
||||||
Instead making lot of abstraction and enumerating them manually, You can rely on lustpress to make less of pain. The current state is FREE to use, all anonymous usage is allowed no authentication and CORS enabled.
|
Many developers consume r18 websites as a source of data when building web applications. However, most of these sites — such as pornhub, redtube, and others — do not provide official APIs or public resources that can be easily integrated into applications.
|
||||||
|
|
||||||
|
As a result, developers often need to implement their own scraping logic, build multiple abstractions, and manually maintain integrations for each site.
|
||||||
|
|
||||||
|
Lustpress aims to simplify this process by providing a unified interface for accessing data across multiple r18 sites. Instead of maintaining separate implementations, developers can rely on Lustpress to reduce complexity and development overhead.
|
||||||
|
|
||||||
|
The current state of the service is **free to use**, meaning anonymous usage is allowed. No authentication is required, and **CORS is enabled** to support browser-based applications.
|
||||||
|
|
||||||
## The solution
|
## The solution
|
||||||
Don't make it long, make it short, all processed through single rest
|
Don't make it long, make it short. All processed through single rest endpoint bindings
|
||||||
<a href="https://sinkaroid.github.io/lustpress"><img src="https://cdn.discordapp.com/attachments/938964058735013899/1097780378061766697/glow.png" width="800"></a>
|
<a href="https://sinkaroid.github.io/lustpress"><img src="resources/project/images/coverage.png" width="800"></a>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Ratelimiting and throttling
|
- Aggregates data from multiple sites.
|
||||||
- Gather the most adult videos on the internet
|
- Provides a consistent and structured response format across all sources.
|
||||||
- Objects taken are consistent structure
|
- Extracted objects are normalized and reassembled to support extensibility.
|
||||||
- Objects taken is re-appended to make extendable
|
- Unified interface supporting **get**, **search**, and **random** methods.
|
||||||
- All in one: get, search, and random methods
|
- Planned support for optional **JWT authentication** in future releases.
|
||||||
- In the future we may implement JWT authentication
|
- Primarily based on pure scraping techniques (with limited exceptions where required).
|
||||||
- Pure scraping, does not hit the API (if exists)
|
|
||||||
|
## Running tests
|
||||||
|
Some tests may fail in CI environments because certain websites restrict or block automated requests originating from CI infrastructure and shared IP ranges, but trying to keep up
|
||||||
|
|
||||||
## Lustpress avaibility
|
|
||||||
**Features availability** that Lustpress has
|
|
||||||
| Site | Status | Get | Search | Random | Related |
|
| Site | Status | Get | Search | Random | Related |
|
||||||
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | ------ | ------ | ------- |
|
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | ------ | ------ | ------- |
|
||||||
| `pornhub` | [](https://github.com/sinkaroid/lustpress/actions/workflows/pornhub.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
|
| `pornhub` | [](https://github.com/sinkaroid/lustpress/actions/workflows/pornhub.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
|
||||||
@@ -68,10 +76,12 @@ Don't make it long, make it short, all processed through single rest
|
|||||||
| `xvideos` | [](https://github.com/sinkaroid/lustpress/actions/workflows/xvideos.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
|
| `xvideos` | [](https://github.com/sinkaroid/lustpress/actions/workflows/xvideos.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
|
||||||
| `xhamster` | [](https://github.com/sinkaroid/lustpress/actions/workflows/xhamster.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
|
| `xhamster` | [](https://github.com/sinkaroid/lustpress/actions/workflows/xhamster.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
|
||||||
| `youporn` | [](https://github.com/sinkaroid/lustpress/actions/workflows/youporn.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
|
| `youporn` | [](https://github.com/sinkaroid/lustpress/actions/workflows/youporn.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
|
||||||
|
| `eporner` | [](https://github.com/sinkaroid/lustpress/actions/workflows/eporner.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
|
||||||
|
| `txxx` | [](https://github.com/sinkaroid/lustpress/actions/workflows/txxx.yml) | `Yes` | `Yes` | `Yes` | `Yes` |
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
<table>
|
<table>
|
||||||
<td><b>NOTE:</b> NodeJS 14.x or higher</td>
|
<td><b>NOTE:</b> NodeJS 22.x or higher</td>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
To handle several requests from each website, You will also need [Redis](https://redis.io/) for persistent caching, free tier is available on [Redis Labs](https://redislabs.com/), You can also choose another adapters as we using [keyv](https://github.com/jaredwray/keyv) Key-value storage with support for multiple backends. When you choosing your own adapter, all data must be stored with `<Buffer>` type.
|
To handle several requests from each website, You will also need [Redis](https://redis.io/) for persistent caching, free tier is available on [Redis Labs](https://redislabs.com/), You can also choose another adapters as we using [keyv](https://github.com/jaredwray/keyv) Key-value storage with support for multiple backends. When you choosing your own adapter, all data must be stored with `<Buffer>` type.
|
||||||
@@ -93,7 +103,7 @@ REDIS_URL = redis://default:somenicepassword@redis-666.c10.us-east-6-6.ec666.clo
|
|||||||
EXPIRE_CACHE = 1
|
EXPIRE_CACHE = 1
|
||||||
|
|
||||||
# you must identify your origin, if not set it will use default
|
# you must identify your origin, if not set it will use default
|
||||||
USER_AGENT = "lustpress/1.6.1 Node.js/16.9.1"
|
USER_AGENT = "lustpress/8.0.1 Node.js/22.22.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
@@ -108,7 +118,7 @@ docker run -d \
|
|||||||
-p 3000:3000 \
|
-p 3000:3000 \
|
||||||
-e REDIS_URL='redis://default:somenicepassword@redis-666.c10.us-east-6-6.ec666.cloud.redislabs.com:1337' \
|
-e REDIS_URL='redis://default:somenicepassword@redis-666.c10.us-east-6-6.ec666.cloud.redislabs.com:1337' \
|
||||||
-e EXPIRE_CACHE='1' \
|
-e EXPIRE_CACHE='1' \
|
||||||
-e USER_AGENT='lustpress/1.6.1 Node.js/16.9.1' \
|
-e USER_AGENT='lustpress/8.0.1 Node.js/22.22.0' \
|
||||||
ghcr.io/sinkaroid/lustpress:latest
|
ghcr.io/sinkaroid/lustpress:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -123,8 +133,28 @@ docker run -d \
|
|||||||
- Lustpress testing and hot reload
|
- Lustpress testing and hot reload
|
||||||
- `npm run start:dev`
|
- `npm run start:dev`
|
||||||
|
|
||||||
## Running tests
|
## Tests
|
||||||
Lustpress testing
|
Run the following commands to execute tests for each supported source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check whether all supported sites are available for scraping
|
||||||
|
npm run test
|
||||||
|
|
||||||
|
# Check whether ph and (maybe the others do..) do Solving challenge in their website
|
||||||
|
npm run test:mock
|
||||||
|
|
||||||
|
# Run tests for individual sources
|
||||||
|
npm run test:pornhub
|
||||||
|
npm run test:xnxx
|
||||||
|
npm run test:redtube
|
||||||
|
npm run test:xvideos
|
||||||
|
npm run test:xhamster
|
||||||
|
npm run test:youporn
|
||||||
|
npm run test:eporner
|
||||||
|
npm run test:txxx
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Start the production server
|
### Start the production server
|
||||||
`npm run start:prod`
|
`npm run start:prod`
|
||||||
@@ -132,10 +162,10 @@ Lustpress testing
|
|||||||
### Running development server
|
### Running development server
|
||||||
`npm run start:dev`
|
`npm run start:dev`
|
||||||
|
|
||||||
### Check the whole sites, It's available for scraping or not
|
### Generating playground like swagger from apidoc definition
|
||||||
`npm run test`
|
`npm run build:apidoc`
|
||||||
|
|
||||||
> To running other tests, you can see object scripts in file `package.json`
|
> To running other tests, you can see object scripts in file `package.json` or modify the `lustpress.test.ts` according your needs
|
||||||
|
|
||||||
## Playground
|
## Playground
|
||||||
https://sinkaroid.github.io/lustpress
|
https://sinkaroid.github.io/lustpress
|
||||||
@@ -154,10 +184,10 @@ The missing piece of pornhub.com - https://sinkaroid.github.io/lustpress/#api-po
|
|||||||
- <u>sort parameters on search</u>
|
- <u>sort parameters on search</u>
|
||||||
- "mr", "mv", "tr", "lg"
|
- "mr", "mv", "tr", "lg"
|
||||||
- Example
|
- Example
|
||||||
- https://lust.scathach.id/pornhub/get?id=ph63c4e1dc48fe7
|
- http://localhost:3000/pornhub/get?id=ph63c4e1dc48fe7
|
||||||
- https://lust.scathach.id/pornhub/search?key=milf&page=2&sort=mr
|
- http://localhost:3000/pornhub/search?key=milf&page=2&sort=mr
|
||||||
- https://lust.scathach.id/pornhub/related?id=ph63c4e1dc48fe7
|
- http://localhost:3000/pornhub/related?id=ph63c4e1dc48fe7
|
||||||
- https://lust.scathach.id/pornhub/random
|
- http://localhost:3000/pornhub/random
|
||||||
|
|
||||||
### Xnxx
|
### Xnxx
|
||||||
The missing piece of xnxx.com - https://sinkaroid.github.io/lustpress/#api-xnxx
|
The missing piece of xnxx.com - https://sinkaroid.github.io/lustpress/#api-xnxx
|
||||||
@@ -169,10 +199,10 @@ The missing piece of xnxx.com - https://sinkaroid.github.io/lustpress/#api-xnxx
|
|||||||
- <u>sort parameters on search</u>
|
- <u>sort parameters on search</u>
|
||||||
- TBD
|
- TBD
|
||||||
- Example
|
- Example
|
||||||
- https://lust.scathach.id/xnxx/get?id=video-17vah71a/makima_y_denji
|
- http://localhost:3000/xnxx/get?id=video-17vah71a/makima_y_denji
|
||||||
- https://lust.scathach.id/xnxx/search?key=bbc&page=2
|
- http://localhost:3000/xnxx/search?key=bbc&page=2
|
||||||
- https://lust.scathach.id/xnxx/related?id=video-17vah71a/makima_y_denji
|
- http://localhost:3000/xnxx/related?id=video-17vah71a/makima_y_denji
|
||||||
- https://lust.scathach.id/xnxx/random
|
- http://localhost:3000/xnxx/random
|
||||||
|
|
||||||
### RedTube
|
### RedTube
|
||||||
The missing piece of redtube.com - https://sinkaroid.github.io/lustpress/#api-redtube
|
The missing piece of redtube.com - https://sinkaroid.github.io/lustpress/#api-redtube
|
||||||
@@ -184,10 +214,10 @@ The missing piece of redtube.com - https://sinkaroid.github.io/lustpress/#api-re
|
|||||||
- <u>sort parameters on search</u>
|
- <u>sort parameters on search</u>
|
||||||
- TBD
|
- TBD
|
||||||
- Example
|
- Example
|
||||||
- https://lust.scathach.id/redtube/get?id=42763661
|
- http://localhost:3000/redtube/get?id=42763661
|
||||||
- https://lust.scathach.id/redtube/search?key=asian&page=2
|
- http://localhost:3000/redtube/search?key=asian&page=2
|
||||||
- https://lust.scathach.id/redtube/related?id=42763661
|
- http://localhost:3000/redtube/related?id=42763661
|
||||||
- https://lust.scathach.id/redtube/random
|
- http://localhost:3000/redtube/random
|
||||||
|
|
||||||
### Xvideos
|
### Xvideos
|
||||||
The missing piece of xvideos.com - https://sinkaroid.github.io/lustpress/#api-xvideos
|
The missing piece of xvideos.com - https://sinkaroid.github.io/lustpress/#api-xvideos
|
||||||
@@ -199,10 +229,10 @@ The missing piece of xvideos.com - https://sinkaroid.github.io/lustpress/#api-xv
|
|||||||
- <u>sort parameters on search</u>
|
- <u>sort parameters on search</u>
|
||||||
- TBD
|
- TBD
|
||||||
- Example
|
- Example
|
||||||
- https://lust.scathach.id/xvideos/get?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_
|
- http://localhost:3000/xvideos/get?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_
|
||||||
- https://lust.scathach.id/xvideos/search?key=hentai&page=2
|
- http://localhost:3000/xvideos/search?key=hentai&page=2
|
||||||
- https://lust.scathach.id/xvideos/related?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_
|
- http://localhost:3000/xvideos/related?id=video73564387/cute_hentai_maid_with_pink_hair_fucking_uncensored_
|
||||||
- https://lust.scathach.id/xvideos/random
|
- http://localhost:3000/xvideos/random
|
||||||
|
|
||||||
### Xhamster
|
### Xhamster
|
||||||
The missing piece of xhamster.com - https://sinkaroid.github.io/lustpress/#api-xhamster
|
The missing piece of xhamster.com - https://sinkaroid.github.io/lustpress/#api-xhamster
|
||||||
@@ -211,13 +241,11 @@ The missing piece of xhamster.com - https://sinkaroid.github.io/lustpress/#api-x
|
|||||||
- **search**, takes parameters : `key`, `?page`, and TBD
|
- **search**, takes parameters : `key`, `?page`, and TBD
|
||||||
- **related**, takes parameters : `id`
|
- **related**, takes parameters : `id`
|
||||||
- **random**
|
- **random**
|
||||||
- <u>sort parameters on search</u>
|
|
||||||
- TBD
|
|
||||||
- Example
|
- Example
|
||||||
- https://lust.scathach.id/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
|
- http://localhost:3000/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
|
||||||
- https://lust.scathach.id/xhamster/search?key=arab&page=2
|
- http://localhost:3000/xhamster/search?key=arab&page=2
|
||||||
- https://lust.scathach.id/xhamster/related?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
|
- http://localhost:3000/xhamster/related?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
|
||||||
- https://lust.scathach.id/xhamster/random
|
- http://localhost:3000/xhamster/random
|
||||||
|
|
||||||
### YouPorn
|
### YouPorn
|
||||||
The missing piece of youporn.com - https://sinkaroid.github.io/lustpress/#api-youporn
|
The missing piece of youporn.com - https://sinkaroid.github.io/lustpress/#api-youporn
|
||||||
@@ -229,22 +257,38 @@ The missing piece of youporn.com - https://sinkaroid.github.io/lustpress/#api-yo
|
|||||||
- <u>sort parameters on search</u>
|
- <u>sort parameters on search</u>
|
||||||
- TBD
|
- TBD
|
||||||
- Example
|
- Example
|
||||||
- https://lust.scathach.id/youporn/get?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps
|
- http://localhost:3000/youporn/get?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps
|
||||||
- https://lust.scathach.id/youporn/search?key=teen&page=2
|
- http://localhost:3000/youporn/search?key=teen&page=2
|
||||||
- https://lust.scathach.id/youporn/related?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps
|
- http://localhost:3000/youporn/related?id=16621192/chainsaw-man-fuck-makima-3d-porn-60-fps
|
||||||
- https://lust.scathach.id/youporn/random
|
- http://localhost:3000/youporn/random
|
||||||
|
|
||||||
### JavHD
|
### Eporner
|
||||||
The missing piece of javhd.com - TBA
|
https://sinkaroid.github.io/lustpress/#api-eporner
|
||||||
- `/javhd` : javhd api
|
- `/eporner` : eporner api
|
||||||
- **get**, takes parameters : TBD
|
- **get**, takes parameters : `id`
|
||||||
- **search**, takes parameters : TBD
|
- **search**, takes parameters : `key`, `?page`
|
||||||
- **related**, takes parameters : TBD
|
- **related**, takes parameters : `id`
|
||||||
- **random**
|
- **random**
|
||||||
- <u>sort parameters on search</u>
|
- <u>sort parameters on search</u>
|
||||||
- TBD
|
- TBD
|
||||||
- Example
|
- Example
|
||||||
- TBD
|
- http://localhost:3000/eporner/get?id=ibvqvezXzcs
|
||||||
|
- http://localhost:3000/eporner/search?key=makima&page=2
|
||||||
|
- http://localhost:3000/eporner/related?id=GPOFlSQLukS
|
||||||
|
- http://localhost:3000/eporner/random
|
||||||
|
|
||||||
|
### Txxx
|
||||||
|
https://sinkaroid.github.io/lustpress/#api-txxx
|
||||||
|
- `/txxx` : txxx api
|
||||||
|
- **get**, takes parameters : `id`
|
||||||
|
- **search**, takes parameters : `key`, `?page`
|
||||||
|
- **related**, takes parameters : `id`
|
||||||
|
- **random**
|
||||||
|
- Example
|
||||||
|
- http://localhost:3000/txxx/get?id=5309183
|
||||||
|
- http://localhost:3000/txxx/search?key=femboy&page=1
|
||||||
|
- http://localhost:3000/txxx/related?id=7794034
|
||||||
|
- http://localhost:3000/txxx/random
|
||||||
|
|
||||||
|
|
||||||
## Status response
|
## Status response
|
||||||
@@ -256,23 +300,23 @@ The missing piece of javhd.com - TBA
|
|||||||
|
|
||||||
## Frequently asked questions
|
## Frequently asked questions
|
||||||
**Q: The website response is slow**
|
**Q: The website response is slow**
|
||||||
> That's unfortunate, This repository was opensource already, You can host and deploy Lustpress with your own instance. Any fixes and improvements will updating to this repo.
|
> That's unfortunate, this repository was opensource already, You can host and deploy Lustpress with your own instance. Any fixes and improvements will updating to this repo.
|
||||||
|
|
||||||
|
> **March 11, 2026**:
|
||||||
|
We have discontinued providing public APIs and playground services due to ongoing abuse and excessive usage.
|
||||||
|
To continue using Lustpress, please deploy and run your own self-hosted instance.
|
||||||
|
|
||||||
**Q: I dont want to host my own instance**
|
|
||||||
> That's unfortunate, Hit the "Sponsor this project" button, any kind of donations will helps me to funding the development.
|
|
||||||
|
|
||||||
## Pronunciation
|
## Pronunciation
|
||||||
`en_US` • **/lʌstˈprɛs/** — "lust" stand for this project and "press" for express.
|
`en_US` • **/lʌstˈprɛs/** — "lust" stand for this project and "press" for express.
|
||||||
|
|
||||||
|
|
||||||
## Client libraries / Wrappers
|
## Client libraries
|
||||||
Seamlessly integrate with the languages you love, simplified the usage, and intelisense definitions on your IDEs
|
Seamlessly integrate with the languages you love, simplified the usage, and intelisense definitions on your IDEs
|
||||||
|
|
||||||
- TBD
|
- TBD
|
||||||
- Or [create your own](https://github.com/sinkaroid/lustpress/edit/master/README.md)
|
- Or [create your own](https://github.com/sinkaroid/lustpress/edit/master/README.md)
|
||||||
|
|
||||||
## Acknowledgements
|
|
||||||
I thank you to all the [Scathach's supporter](https://scathachbot.xyz/about) who made this project exists :gajah_ngecrot:
|
|
||||||
|
|
||||||
## Legal
|
## Legal
|
||||||
This tool can be freely copied, modified, altered, distributed without any attribution whatsoever. However, if you feel
|
This tool can be freely copied, modified, altered, distributed without any attribution whatsoever. However, if you feel
|
||||||
|
|||||||
45
eslint.config.js
Normal file
45
eslint.config.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const tsParser = require("@typescript-eslint/parser");
|
||||||
|
const tsPlugin = require("@typescript-eslint/eslint-plugin");
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
ignores: ["node_modules/**", "build/**"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.ts"],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 12,
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
module: "readonly",
|
||||||
|
require: "readonly",
|
||||||
|
__dirname: "readonly",
|
||||||
|
__filename: "readonly",
|
||||||
|
process: "readonly",
|
||||||
|
Buffer: "readonly",
|
||||||
|
console: "readonly",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"@typescript-eslint": tsPlugin,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"no-unused-vars": "error",
|
||||||
|
"no-undef": "error",
|
||||||
|
"linebreak-style": 0,
|
||||||
|
"quotes": ["error", "double"],
|
||||||
|
"semi": ["error", "always"],
|
||||||
|
"no-empty": "error",
|
||||||
|
"no-func-assign": "error",
|
||||||
|
"no-case-declarations": "off",
|
||||||
|
"no-unreachable": "error",
|
||||||
|
"no-eval": "error",
|
||||||
|
"no-global-assign": "error",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"indent": ["error", 2],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
77
package.json
77
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "lustpress",
|
"name": "lustpress",
|
||||||
"version": "1.6.1-alpha",
|
"version": "8.1.2-alpha",
|
||||||
"description": "RESTful and experimental API for PornHub and other porn sites, which official is lack even isn't exist.",
|
"description": "RESTful and experimental API for PH and other R18 websites",
|
||||||
"main": "build/src/index.js",
|
"main": "build/src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rimraf build && tsc",
|
"build": "rimraf build && tsc",
|
||||||
@@ -13,20 +13,21 @@
|
|||||||
"start:flyctl": "flyctl deploy",
|
"start:flyctl": "flyctl deploy",
|
||||||
"lint": "eslint . --ext .ts",
|
"lint": "eslint . --ext .ts",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"build:freshdoc": "rimraf docs && rimraf template && rimraf theme.zip",
|
"build:freshdoc": "rimraf docs",
|
||||||
"build:template": " npm run build:freshdoc && curl https://codeload.github.com/ScathachGrip/apidocjs-theme/zip/refs/tags/v11 -o theme.zip && unzip theme.zip && mv apidocjs-theme-11 template",
|
"build:apidoc": "npm run build:freshdoc && npx apidoc -i src -o docs",
|
||||||
"build:apidoc": "npm run build:template && apidoc -i src -o ./docs -t ./template/template-scarlet",
|
"test:pornhub": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^pornhub$ test/lustpress.test.ts\"",
|
||||||
"test:pornhub": "start-server-and-test 3000 \"curl -v http://localhost:3000/pornhub/random | jq '.'\"",
|
"test:xnxx": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^xnxx$ test/lustpress.test.ts\"",
|
||||||
"test:xnxx": "start-server-and-test 3000 \"curl -v http://localhost:3000/xnxx/random | jq '.'\"",
|
"test:redtube": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^redtube$ test/lustpress.test.ts\"",
|
||||||
"test:redtube": "start-server-and-test 3000 \"curl -v http://localhost:3000/redtube/random | jq '.'\"",
|
"test:xvideos": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^xvideos$ test/lustpress.test.ts\"",
|
||||||
"test:xvideos": "start-server-and-test 3000 \"curl -v http://localhost:3000/xvideos/random | jq '.'\"",
|
"test:xhamster": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^xhamster$ test/lustpress.test.ts\"",
|
||||||
"test:xhamster": "start-server-and-test 3000 \"curl -v http://localhost:3000/xhamster/random | jq '.'\"",
|
"test:youporn": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^youporn$ test/lustpress.test.ts\"",
|
||||||
"test:youporn": "start-server-and-test 3000 \"curl -v http://localhost:3000/youporn/random | jq '.'\""
|
"test:eporner": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^eporner$ test/lustpress.test.ts\"",
|
||||||
|
"test:txxx": "start-server-and-test \"npm run start:dev\" 3000 \"node -r ts-node/register --test --test-name-pattern=^txxx$ test/lustpress.test.ts\""
|
||||||
},
|
},
|
||||||
"apidoc": {
|
"apidoc": {
|
||||||
"title": "Lustpress API Documentation",
|
"title": "Lustpress API Documentation",
|
||||||
"url": "https://lust.scathach.id",
|
"url": "http://localhost:3000",
|
||||||
"sampleUrl": "https://lust.scathach.id",
|
"sampleUrl": "http://localhost:3000",
|
||||||
"name": "Lustpress"
|
"name": "Lustpress"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
@@ -37,34 +38,34 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@keyv/redis": "^2.5.7",
|
"@keyv/redis": "^5.1.6",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.2.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.6",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^17.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^5.2.1",
|
||||||
"express-rate-limit": "^6.7.0",
|
"express-rate-limit": "^8.3.1",
|
||||||
"express-slow-down": "^1.6.0",
|
"express-slow-down": "^3.1.0",
|
||||||
"keyv": "^4.5.2",
|
"keyv": "^5.6.0",
|
||||||
"phin": "^3.7.0",
|
"phin": "^3.7.1",
|
||||||
"pino": "^8.11.0",
|
"pino": "^10.3.1",
|
||||||
"pino-pretty": "^10.0.0"
|
"pino-pretty": "^13.1.3",
|
||||||
|
"playwright": "^1.58.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.13",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^5.0.6",
|
||||||
"@types/express-slow-down": "^1.3.2",
|
"@types/node": "^25.4.0",
|
||||||
"@types/node": "^18.15.11",
|
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.58.0",
|
"@typescript-eslint/parser": "^8.57.0",
|
||||||
"@typescript-eslint/parser": "^5.58.0",
|
"apidoc": "^1.2.0",
|
||||||
"apidoc": "0.29.0",
|
"eslint": "^10.0.3",
|
||||||
"eslint": "^8.38.0",
|
"rimraf": "^6.1.3",
|
||||||
"rimraf": "^5.0.0",
|
"start-server-and-test": "^2.1.5",
|
||||||
"start-server-and-test": "^2.0.0",
|
"ts-node": "^10.9.2",
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.9.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=22"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BIN
resources/project/images/bnnuy.png
Normal file
BIN
resources/project/images/bnnuy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
BIN
resources/project/images/coverage.png
Normal file
BIN
resources/project/images/coverage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 990 KiB |
BIN
resources/project/images/lustpress-node_1.png
Normal file
BIN
resources/project/images/lustpress-node_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 206 KiB |
157
src/LustPress.ts
157
src/LustPress.ts
@@ -1,53 +1,159 @@
|
|||||||
|
import { URL } from "node:url";
|
||||||
|
import { chromium } from "playwright";
|
||||||
import p, { IResponse } from "phin";
|
import p, { IResponse } from "phin";
|
||||||
import Keyv from "keyv";
|
import Keyv from "keyv";
|
||||||
|
import KeyvRedis from "@keyv/redis";
|
||||||
import pkg from "../package.json";
|
import pkg from "../package.json";
|
||||||
|
|
||||||
|
const keyv = process.env.REDIS_URL
|
||||||
const keyv = new Keyv(process.env.REDIS_URL);
|
? new Keyv({ store: new KeyvRedis(process.env.REDIS_URL) })
|
||||||
|
: new Keyv();
|
||||||
|
|
||||||
keyv.on("error", err => console.log("Connection Error", err));
|
keyv.on("error", err => console.log("Connection Error", err));
|
||||||
const ttl = 1000 * 60 * 60 * Number(process.env.EXPIRE_CACHE);
|
const ttl = 1000 * 60 * 60 * Number(process.env.EXPIRE_CACHE);
|
||||||
|
|
||||||
|
|
||||||
class LustPress {
|
class LustPress {
|
||||||
|
|
||||||
url: string;
|
url: string;
|
||||||
useragent: string;
|
useragent: string;
|
||||||
|
private cookieCache: { [domain: string]: string } = {};
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.url = "";
|
this.url = "";
|
||||||
this.useragent = `${pkg.name}/${pkg.version} Node.js/16.9.1`;
|
this.useragent = `${pkg.name}/${pkg.version} Node.js/${process.versions.node}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCookies(url: string): Promise<string> {
|
||||||
|
const browser = await chromium.launch({ headless: true });
|
||||||
|
|
||||||
|
const context = await browser.newContext({
|
||||||
|
userAgent: process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/${process.versions.node}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await context.newPage();
|
||||||
|
await page.goto(url, { waitUntil: "domcontentloaded" });
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
const cookies = await context.cookies();
|
||||||
|
await browser.close();
|
||||||
|
return cookies.map(c => `${c.name}=${c.value}`).join("; ");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch body from url and check if it's cached
|
* Fetch body from url and check if it's cached
|
||||||
* @param url url to fetch
|
* @param url url to fetch
|
||||||
* @returns Buffer
|
* @returns Buffer
|
||||||
*/
|
*/
|
||||||
async fetchBody(url: string): Promise<Buffer> {
|
async fetchBody(url: string): Promise<Buffer> {
|
||||||
|
|
||||||
const cached = await keyv.get(url);
|
const cached = await keyv.get(url);
|
||||||
|
|
||||||
if (cached) {
|
if (cached) {
|
||||||
console.log("Fetching from cache");
|
console.log("Fetching from cache");
|
||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
} else if (url.includes("/random")) {
|
} else if (url.includes("/random")) {
|
||||||
|
|
||||||
console.log("Random should not be cached");
|
console.log("Random should not be cached");
|
||||||
const res = await p({
|
|
||||||
|
const isPornhub = /pornhub\.com/i.test(url);
|
||||||
|
|
||||||
|
let cookieHeader = "";
|
||||||
|
|
||||||
|
if (isPornhub) {
|
||||||
|
const domain = new URL(url).hostname;
|
||||||
|
|
||||||
|
if (this.cookieCache[domain]) {
|
||||||
|
console.log("Using cached cookie");
|
||||||
|
cookieHeader = this.cookieCache[domain];
|
||||||
|
} else {
|
||||||
|
console.log("Solving challenge via playwright");
|
||||||
|
cookieHeader = await this.getCookies(url);
|
||||||
|
this.cookieCache[domain] = cookieHeader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Fetching from ${isPornhub ? "phin (cookie)" : "phin"}`);
|
||||||
|
|
||||||
|
const res = await p({
|
||||||
url: url,
|
url: url,
|
||||||
"headers": {
|
headers: {
|
||||||
"User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/16.9.1`,
|
"User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/${process.versions.node}`,
|
||||||
},
|
...(cookieHeader && { "Cookie": cookieHeader })
|
||||||
followRedirects: true
|
|
||||||
});
|
|
||||||
return res.body;
|
|
||||||
} else {
|
|
||||||
console.log("Fetching from source");
|
|
||||||
const res = await p({
|
|
||||||
url: url,
|
|
||||||
"headers": {
|
|
||||||
"User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/16.9.1`,
|
|
||||||
},
|
},
|
||||||
followRedirects: true
|
followRedirects: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isPornhub && res.statusCode !== 200) {
|
||||||
|
const domain = new URL(url).hostname;
|
||||||
|
console.log("Cookie invalid, clearing cache and retrying via playwright");
|
||||||
|
delete this.cookieCache[domain];
|
||||||
|
const newCookie = await this.getCookies(url);
|
||||||
|
this.cookieCache[domain] = newCookie;
|
||||||
|
const retry = await p({
|
||||||
|
url: url,
|
||||||
|
headers: {
|
||||||
|
"User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/${process.versions.node}`,
|
||||||
|
"Cookie": newCookie
|
||||||
|
},
|
||||||
|
followRedirects: true
|
||||||
|
});
|
||||||
|
return retry.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.body;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
console.log("Fetching from source");
|
||||||
|
url = url.replace(/\/\//g, "/");
|
||||||
|
const isPornhub = /pornhub\.com/i.test(url);
|
||||||
|
|
||||||
|
let cookieHeader = "";
|
||||||
|
|
||||||
|
if (isPornhub) {
|
||||||
|
const domain = new URL(url).hostname;
|
||||||
|
|
||||||
|
if (this.cookieCache[domain]) {
|
||||||
|
console.log("Using cached cookie");
|
||||||
|
cookieHeader = this.cookieCache[domain];
|
||||||
|
} else {
|
||||||
|
console.log("Solving challenge via playwright");
|
||||||
|
cookieHeader = await this.getCookies(url);
|
||||||
|
this.cookieCache[domain] = cookieHeader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Fetching from ${isPornhub ? "phin (cookie)" : "phin"}`);
|
||||||
|
|
||||||
|
const res = await p({
|
||||||
|
url: url,
|
||||||
|
headers: {
|
||||||
|
"User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/16.9.1`,
|
||||||
|
...(cookieHeader && { "Cookie": cookieHeader })
|
||||||
|
},
|
||||||
|
followRedirects: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isPornhub && res.statusCode !== 200) {
|
||||||
|
const domain = new URL(url).hostname;
|
||||||
|
console.log("Cookie invalid, clearing cache and retrying via playwright");
|
||||||
|
delete this.cookieCache[domain];
|
||||||
|
const newCookie = await this.getCookies(url);
|
||||||
|
this.cookieCache[domain] = newCookie;
|
||||||
|
const retry = await p({
|
||||||
|
url: url,
|
||||||
|
headers: {
|
||||||
|
"User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/16.9.1`,
|
||||||
|
"Cookie": newCookie
|
||||||
|
},
|
||||||
|
followRedirects: true
|
||||||
|
});
|
||||||
|
return retry.body;
|
||||||
|
}
|
||||||
|
|
||||||
await keyv.set(url, res.body, ttl);
|
await keyv.set(url, res.body, ttl);
|
||||||
|
|
||||||
return res.body;
|
return res.body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +177,7 @@ class LustPress {
|
|||||||
removeHtmlTagWithoutSpace(str: string): string {
|
removeHtmlTagWithoutSpace(str: string): string {
|
||||||
str = str.replace(/(\r\n|\n|\r|\t)/gm, "");
|
str = str.replace(/(\r\n|\n|\r|\t)/gm, "");
|
||||||
str = str.replace(/\\/g, "");
|
str = str.replace(/\\/g, "");
|
||||||
|
str = str.replace(/\s+/g, " ");
|
||||||
return str.trim();
|
return str.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +215,7 @@ class LustPress {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* convert seconds to minute
|
* convert seconds to minute
|
||||||
* @param seconds seconds to convert
|
* @param seconds seconds to convert
|
||||||
@@ -141,9 +248,9 @@ class LustPress {
|
|||||||
* @returns <Promise<string>>
|
* @returns <Promise<string>>
|
||||||
*/
|
*/
|
||||||
async getServer(): Promise<string> {
|
async getServer(): Promise<string> {
|
||||||
const raw = await p({
|
const raw = await p({
|
||||||
"url": "http://ip-api.com/json",
|
"url": "http://ip-api.com/json",
|
||||||
"parse": "json"
|
"parse": "json"
|
||||||
}) as IResponse;
|
}) as IResponse;
|
||||||
const data = raw.body as unknown as { country: string, regionName: string };
|
const data = raw.body as unknown as { country: string, regionName: string };
|
||||||
return `${data.country}, ${data.regionName}`;
|
return `${data.country}, ${data.regionName}`;
|
||||||
|
|||||||
66
src/controller/eporner/epornerGet.ts
Normal file
66
src/controller/eporner/epornerGet.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { scrapeContent } from "../../scraper/eporner/epornerGetController";
|
||||||
|
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) {
|
||||||
|
try {
|
||||||
|
const id = req.query.id as string;
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (id.startsWith("video-")) {
|
||||||
|
path = id;
|
||||||
|
} else {
|
||||||
|
path = `hd-porn/${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${c.EPORNER}/${path}`;
|
||||||
|
|
||||||
|
const data = await scrapeContent(url);
|
||||||
|
logger.info({
|
||||||
|
path: req.path,
|
||||||
|
query: req.query,
|
||||||
|
method: req.method,
|
||||||
|
ip: req.ip,
|
||||||
|
useragent: req.get("User-Agent"),
|
||||||
|
});
|
||||||
|
return res.json(data);
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error;
|
||||||
|
res.status(400).json(maybeError(false, e.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/controller/eporner/epornerGetRelated.ts
Normal file
28
src/controller/eporner/epornerGetRelated.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { scrapeContent } from "../../scraper/eporner/epornerGetRelatedController";
|
||||||
|
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) {
|
||||||
|
try {
|
||||||
|
const id = req.query.id as string;
|
||||||
|
if (!id) throw Error("Parameter id is required");
|
||||||
|
|
||||||
|
const url = `${c.EPORNER}/video-${id}`;
|
||||||
|
const data = await scrapeContent(url);
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
path: req.path,
|
||||||
|
query: req.query,
|
||||||
|
method: req.method,
|
||||||
|
ip: req.ip,
|
||||||
|
useragent: req.get("User-Agent")
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json(data);
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error;
|
||||||
|
res.status(400).json(maybeError(false, e.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/controller/eporner/epornerRandom.ts
Normal file
72
src/controller/eporner/epornerRandom.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { scrapeContent as searchScrape } from "../../scraper/eporner/epornerSearchController";
|
||||||
|
import { scrapeContent as videoScrape } from "../../scraper/eporner/epornerGetController";
|
||||||
|
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/
|
||||||
|
|
||||||
|
try {
|
||||||
|
const weeklyUrl = `${c.EPORNER}/cat/all/SORT-top-weekly/`;
|
||||||
|
const list = await searchScrape(weeklyUrl);
|
||||||
|
|
||||||
|
if (!list.data.length) {
|
||||||
|
throw new Error("No weekly top videos found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const random = list.data[Math.floor(Math.random() * list.data.length)];
|
||||||
|
|
||||||
|
let path: string;
|
||||||
|
|
||||||
|
if (random.id.startsWith("video-")) {
|
||||||
|
path = random.id;
|
||||||
|
} else {
|
||||||
|
path = `hd-porn/${random.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${c.EPORNER}/${path}`;
|
||||||
|
const data = await videoScrape(url);
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
path: req.path,
|
||||||
|
method: req.method,
|
||||||
|
ip: req.ip,
|
||||||
|
useragent: req.get("User-Agent"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json(data);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json(maybeError(false, (err as Error).message));
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/controller/eporner/epornerSearch.ts
Normal file
72
src/controller/eporner/epornerSearch.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { scrapeContent } from "../../scraper/eporner/epornerSearchController";
|
||||||
|
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) {
|
||||||
|
try {
|
||||||
|
/**
|
||||||
|
* @api {get} /eporner/search Search eporner videos
|
||||||
|
* @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
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, "-");
|
||||||
|
|
||||||
|
const url =
|
||||||
|
page === 1
|
||||||
|
? `${c.EPORNER}/tag/${slug}/`
|
||||||
|
: `${c.EPORNER}/tag/${slug}/${page}/`;
|
||||||
|
|
||||||
|
const data = await scrapeContent(url);
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
path: req.path,
|
||||||
|
query: req.query,
|
||||||
|
method: req.method,
|
||||||
|
ip: req.ip,
|
||||||
|
useragent: req.get("User-Agent")
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json(data);
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error;
|
||||||
|
res.status(400).json(maybeError(false, e.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/controller/txxx/txxxGet.ts
Normal file
82
src/controller/txxx/txxxGet.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import LustPress from "../../LustPress";
|
||||||
|
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
|
||||||
|
function getApiUrl(videoId: string): string {
|
||||||
|
const id = Number(videoId);
|
||||||
|
if (Number.isNaN(id)) throw new Error("Invalid video id");
|
||||||
|
|
||||||
|
const million = Math.floor(id / 1_000_000) * 1_000_000;
|
||||||
|
const thousand = Math.floor(id / 1_000) * 1_000;
|
||||||
|
|
||||||
|
return `https://txxx.com/api/json/video/86400/${million}/${thousand}/${id}.json`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTxxx(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const id = String(req.query.id || "").trim();
|
||||||
|
if (!id) throw new Error("Parameter id is required");
|
||||||
|
|
||||||
|
const apiUrl = getApiUrl(id);
|
||||||
|
|
||||||
|
const buffer = await lust.fetchBody(apiUrl);
|
||||||
|
const parsed = JSON.parse(buffer.toString("utf-8"));
|
||||||
|
|
||||||
|
if (!parsed?.video) {
|
||||||
|
throw new Error("Invalid API response");
|
||||||
|
}
|
||||||
|
|
||||||
|
const video = parsed.video;
|
||||||
|
|
||||||
|
const categories = Object.values(video.categories || {}).map(
|
||||||
|
(c: any) => c.title,
|
||||||
|
);
|
||||||
|
|
||||||
|
const tags = Object.values(video.tags || {}).map((t: any) => t.title);
|
||||||
|
|
||||||
|
const models = Object.values(video.models || {}).map((m: any) => m.title);
|
||||||
|
|
||||||
|
const videoDir = video.dir || "";
|
||||||
|
const videoId = video.video_id;
|
||||||
|
|
||||||
|
const embed = `https://txxx.com/embed/${videoId}/`;
|
||||||
|
|
||||||
|
const response: IVideoData = {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
title: video.title || "None",
|
||||||
|
id: `${videoId}`,
|
||||||
|
image: video.thumbsrc || video.thumb || "None",
|
||||||
|
duration: video.duration || "None",
|
||||||
|
views: video.statistics?.viewed || "0",
|
||||||
|
rating: video.statistics?.rating || "0.00",
|
||||||
|
uploaded: video.post_date || "None",
|
||||||
|
upvoted: String(video.statistics?.likes || "0"),
|
||||||
|
downvoted: String(video.statistics?.dislikes || "0"),
|
||||||
|
channel: video.channel?.title || "",
|
||||||
|
models: models.length > 0 ? models : video.models_suggested || [],
|
||||||
|
tags: [...categories, ...tags],
|
||||||
|
},
|
||||||
|
source: `https://txxx.com/videos/${videoId}/${videoDir}/`,
|
||||||
|
assets: [embed, video.thumbsrc].filter(Boolean),
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
path: req.path,
|
||||||
|
query: req.query,
|
||||||
|
method: req.method,
|
||||||
|
ip: req.ip,
|
||||||
|
useragent: req.get("User-Agent"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json(response);
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error;
|
||||||
|
return res.status(400).json(maybeError(false, e.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/controller/txxx/txxxGetRelated.ts
Normal file
69
src/controller/txxx/txxxGetRelated.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import LustPress from "../../LustPress";
|
||||||
|
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 {
|
||||||
|
const id = Number(videoId);
|
||||||
|
if (Number.isNaN(id)) throw new Error("Invalid video id");
|
||||||
|
|
||||||
|
const million = Math.floor(id / 1_000_000) * 1_000_000;
|
||||||
|
const thousand = Math.floor(id / 1_000) * 1_000;
|
||||||
|
|
||||||
|
return (
|
||||||
|
"https://txxx.com/api/json/videos_related2/" +
|
||||||
|
`432000/${count}/${million}/${thousand}/${id}.all.${page}.json`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function relatedTxxx(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const id = String(req.query.id || "").trim();
|
||||||
|
const page = Number(req.query.page || 1);
|
||||||
|
|
||||||
|
if (!id) throw new Error("Parameter id is required");
|
||||||
|
if (Number.isNaN(page)) throw new Error("Parameter page must be a number");
|
||||||
|
|
||||||
|
const apiUrl = getRelatedApiUrl(id, page);
|
||||||
|
|
||||||
|
const buffer = await lust.fetchBody(apiUrl);
|
||||||
|
const rawData = JSON.parse(buffer.toString("utf-8"));
|
||||||
|
|
||||||
|
const videos = Array.isArray(rawData.videos) ? rawData.videos : [];
|
||||||
|
|
||||||
|
const data = videos.map((v: any) => ({
|
||||||
|
id: v.video_id,
|
||||||
|
title: v.title,
|
||||||
|
image: v.scr || v.thumb || null,
|
||||||
|
duration: v.duration || "None",
|
||||||
|
views: v.video_viewed || "0",
|
||||||
|
rating: v.rating || "0",
|
||||||
|
uploader: v.username || v.display_name || "",
|
||||||
|
link: `https://txxx.com/videos/${v.video_id}/${v.dir}/`,
|
||||||
|
video: `https://txxx.com/embed/${v.video_id}/`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
path: req.path,
|
||||||
|
query: req.query,
|
||||||
|
method: req.method,
|
||||||
|
ip: req.ip,
|
||||||
|
useragent: req.get("User-Agent"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
total_count: rawData.total_count || "0",
|
||||||
|
pages: rawData.pages || 1,
|
||||||
|
page,
|
||||||
|
data,
|
||||||
|
source: `https://txxx.com/videos/${id}/`,
|
||||||
|
} as ISearchVideoData);
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error;
|
||||||
|
return res.status(400).json(maybeError(false, e.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/controller/txxx/txxxRandom.ts
Normal file
58
src/controller/txxx/txxxRandom.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import LustPress from "../../LustPress";
|
||||||
|
import { maybeError } from "../../utils/modifier";
|
||||||
|
import { logger } from "../../utils/logger";
|
||||||
|
|
||||||
|
const lust = new LustPress();
|
||||||
|
|
||||||
|
export async function randomTxxx(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const apiUrl =
|
||||||
|
"https://txxx.com/api/json/videos2/14400/str/most-popular/60/..1.all..day.json";
|
||||||
|
|
||||||
|
const buffer = await lust.fetchBody(apiUrl);
|
||||||
|
const rawData = JSON.parse(buffer.toString("utf-8"));
|
||||||
|
|
||||||
|
const videos = Array.isArray(rawData.videos)
|
||||||
|
? rawData.videos
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (videos.length === 0) {
|
||||||
|
throw new Error("No videos returned from upstream");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick one random video
|
||||||
|
const v = videos[Math.floor(Math.random() * videos.length)];
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
video_id: v.video_id,
|
||||||
|
title: v.title,
|
||||||
|
dir: v.dir,
|
||||||
|
duration: v.duration,
|
||||||
|
views: v.video_viewed,
|
||||||
|
rating: v.rating,
|
||||||
|
uploaded: v.post_date,
|
||||||
|
likes: v.likes,
|
||||||
|
dislikes: v.dislikes,
|
||||||
|
image: v.scr,
|
||||||
|
categories: v.categories ? v.categories.split(",") : [],
|
||||||
|
embed: `https://txxx.com/embed/${v.video_id}/`,
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
path: req.path,
|
||||||
|
method: req.method,
|
||||||
|
ip: req.ip,
|
||||||
|
useragent: req.get("User-Agent"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
source: apiUrl,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error;
|
||||||
|
return res.status(400).json(maybeError(false, e.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/controller/txxx/txxxSearch.ts
Normal file
63
src/controller/txxx/txxxSearch.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import LustPress from "../../LustPress";
|
||||||
|
import { maybeError } from "../../utils/modifier";
|
||||||
|
|
||||||
|
const lust = new LustPress();
|
||||||
|
|
||||||
|
export async function searchTxxx(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const key = String(req.query.key || "").trim();
|
||||||
|
const page = Number(req.query.page || 1);
|
||||||
|
|
||||||
|
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 =
|
||||||
|
"https://txxx.com/api/videos2.php" +
|
||||||
|
`?params=259200/str/relevance/60/search..${page}.all..` +
|
||||||
|
`&s=${encodeURIComponent(key)}`;
|
||||||
|
|
||||||
|
// Fetch from API directly
|
||||||
|
const buffer = await lust.fetchBody(apiUrl);
|
||||||
|
const rawData = JSON.parse(buffer.toString("utf-8"));
|
||||||
|
|
||||||
|
const videos = Array.isArray(rawData.videos) ? rawData.videos : [];
|
||||||
|
|
||||||
|
const data = videos.map((v: any) => ({
|
||||||
|
video_id: v.video_id,
|
||||||
|
title: v.title,
|
||||||
|
dir: v.dir,
|
||||||
|
duration: v.duration,
|
||||||
|
views: v.video_viewed,
|
||||||
|
rating: v.rating,
|
||||||
|
uploaded: v.post_date,
|
||||||
|
likes: v.likes,
|
||||||
|
dislikes: v.dislikes,
|
||||||
|
image: v.scr,
|
||||||
|
categories: v.categories ? v.categories.split(",") : [],
|
||||||
|
embed: `https://txxx.com/embed/${v.video_id}/`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
total_count: String(rawData.total_count ?? videos.length),
|
||||||
|
pages: Number(rawData.pages ?? 1),
|
||||||
|
page,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error;
|
||||||
|
return res.json(maybeError(false, e.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,23 +14,23 @@ export async function getXhamster(req: Request, res: Response) {
|
|||||||
* @apiName Get xhamster
|
* @apiName Get xhamster
|
||||||
* @apiGroup xhamster
|
* @apiGroup xhamster
|
||||||
* @apiDescription Get a xhamster video based on id
|
* @apiDescription Get a xhamster video based on id
|
||||||
*
|
*
|
||||||
* @apiParam {String} id Video ID
|
* @apiParam {String} id Video ID
|
||||||
*
|
*
|
||||||
* @apiSuccessExample {json} Success-Response:
|
* @apiSuccessExample {json} Success-Response:
|
||||||
* HTTP/1.1 200 OK
|
* HTTP/1.1 200 OK
|
||||||
* HTTP/1.1 400 Bad Request
|
* HTTP/1.1 400 Bad Request
|
||||||
*
|
*
|
||||||
* @apiExample {curl} curl
|
* @apiExample {curl} curl
|
||||||
* curl -i https://lust.scathach.id/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
|
* curl -i https://lust.scathach.id/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
|
||||||
*
|
*
|
||||||
* @apiExample {js} JS/TS
|
* @apiExample {js} JS/TS
|
||||||
* import axios from "axios"
|
* import axios from "axios"
|
||||||
*
|
*
|
||||||
* axios.get("https://lust.scathach.id/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx")
|
* 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))
|
* .then(res => console.log(res.data))
|
||||||
* .catch(err => console.error(err))
|
* .catch(err => console.error(err))
|
||||||
*
|
*
|
||||||
* @apiExample {python} Python
|
* @apiExample {python} Python
|
||||||
* import aiohttp
|
* import aiohttp
|
||||||
* async with aiohttp.ClientSession() as session:
|
* async with aiohttp.ClientSession() as session:
|
||||||
@@ -38,14 +38,14 @@ export async function getXhamster(req: Request, res: Response) {
|
|||||||
* print(await resp.json())
|
* print(await resp.json())
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const url = `${c.XHAMSTER}/${id}`;
|
const url = `${c.XHAMSTER}/videos/${id}`;
|
||||||
const data = await scrapeContent(url);
|
const data = await scrapeContent(url);
|
||||||
logger.info({
|
logger.info({
|
||||||
path: req.path,
|
path: req.path,
|
||||||
query: req.query,
|
query: req.query,
|
||||||
method: req.method,
|
method: req.method,
|
||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
useragent: req.get("User-Agent")
|
useragent: req.get("User-Agent"),
|
||||||
});
|
});
|
||||||
return res.json(data);
|
return res.json(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -10,51 +10,58 @@ const lust = new LustPress();
|
|||||||
|
|
||||||
export async function randomXhamster(req: Request, res: Response) {
|
export async function randomXhamster(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} /xhamster/random Get random xhamster
|
* @api {get} /xhamster/random Get random xhamster video
|
||||||
* @apiName Get random xhamster
|
* @apiName Get random xhamster
|
||||||
* @apiGroup xhamster
|
* @apiGroup xhamster
|
||||||
* @apiDescription Get a random xhamster video
|
* @apiDescription Get a random xhamster video from the list of newest videos.
|
||||||
*
|
*
|
||||||
* @apiSuccessExample {json} Success-Response:
|
* @apiSuccessExample {json} Success-Response:
|
||||||
* HTTP/1.1 200 OK
|
* HTTP/1.1 200 OK
|
||||||
* HTTP/1.1 400 Bad Request
|
* HTTP/1.1 400 Bad Request
|
||||||
*
|
*
|
||||||
* @apiExample {curl} curl
|
* @apiExample {curl} curl
|
||||||
* curl -i https://lust.scathach.id/xhamster/random
|
* curl -i https://lust.scathach.id/xhamster/random
|
||||||
*
|
*
|
||||||
* @apiExample {js} JS/TS
|
* @apiExample {js} JS/TS
|
||||||
* import axios from "axios"
|
* import axios from "axios"
|
||||||
*
|
*
|
||||||
* axios.get("https://lust.scathach.id/xhamster/random")
|
* axios.get("https://lust.scathach.id/xhamster/random")
|
||||||
* .then(res => console.log(res.data))
|
* .then(res => console.log(res.data))
|
||||||
* .catch(err => console.error(err))
|
* .catch(err => console.error(err))
|
||||||
*
|
*
|
||||||
* @apiExample {python} Python
|
* @apiExample {python} Python
|
||||||
* import aiohttp
|
* import aiohttp
|
||||||
* async with aiohttp.ClientSession() as session:
|
* async with aiohttp.ClientSession() as session:
|
||||||
* async with session.get("https://lust.scathach.id/xhamster/random") as resp:
|
* async with session.get("https://lust.scathach.id/xhamster/random") as resp:
|
||||||
* print(await resp.json())
|
* print(await resp.json())
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const resolve = await lust.fetchBody(`${c.XHAMSTER}/newest`);
|
const resolve = await lust.fetchBody(`${c.XHAMSTER}/newest`);
|
||||||
const $ = load(resolve);
|
const $ = load(resolve);
|
||||||
const search = $("a.root-9d8b4.video-thumb-info__name.role-pop.with-dropdown")
|
|
||||||
.map((i, el) => $(el).attr("href"))
|
|
||||||
.get();
|
|
||||||
|
|
||||||
const search_ = search.map((el) => el.replace(c.XHAMSTER, ""));
|
const videoLinks = $("div.thumb-list__item[data-video-id]")
|
||||||
const random = Math.floor(Math.random() * search_.length);
|
.map((_, el) => {
|
||||||
const url = c.XHAMSTER + search_[random];
|
const href = $(el).find("a[data-role='thumb-link']").attr("href");
|
||||||
const data = await scrapeContent(url);
|
return href && href.includes("/videos/") ? href : null;
|
||||||
|
})
|
||||||
|
.get()
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// Select a random video URL from the list
|
||||||
|
const randomIndex = Math.floor(Math.random() * videoLinks.length);
|
||||||
|
const randomUrl = videoLinks[randomIndex];
|
||||||
|
|
||||||
|
const data = await scrapeContent(randomUrl);
|
||||||
|
|
||||||
logger.info({
|
logger.info({
|
||||||
path: req.path,
|
path: req.path,
|
||||||
query: req.query,
|
query: req.query,
|
||||||
method: req.method,
|
method: req.method,
|
||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
useragent: req.get("User-Agent")
|
useragent: req.get("User-Agent"),
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.json(data);
|
return res.json(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as Error;
|
const e = err as Error;
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ export interface IVideoData {
|
|||||||
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;
|
||||||
models: string[];
|
models: string[];
|
||||||
tags: string[];
|
tags: string[];
|
||||||
};
|
};
|
||||||
@@ -23,6 +24,19 @@ export interface ISearchVideoData {
|
|||||||
source: string;
|
source: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ISearchItem {
|
||||||
|
link: string;
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
image?: string;
|
||||||
|
duration?: string;
|
||||||
|
rating?: string;
|
||||||
|
views?: string;
|
||||||
|
uploader?: string;
|
||||||
|
video?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface MaybeError {
|
export interface MaybeError {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,19 @@ import cors from "cors";
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { slow, limiter } from "../utils/limit-options";
|
import { slow, limiter } from "../utils/limit-options";
|
||||||
|
|
||||||
|
|
||||||
|
// EPorner
|
||||||
|
import { getEporner } from "../controller/eporner/epornerGet";
|
||||||
|
import { searchEporner } from "../controller/eporner/epornerSearch";
|
||||||
|
import { relatedEporner } from "../controller/eporner/epornerGetRelated";
|
||||||
|
import { randomEporner } from "../controller/eporner/epornerRandom";
|
||||||
|
|
||||||
|
// TXXX
|
||||||
|
import { getTxxx } from "../controller/txxx/txxxGet";
|
||||||
|
import { searchTxxx } from "../controller/txxx/txxxSearch";
|
||||||
|
import { relatedTxxx } from "../controller/txxx/txxxGetRelated";
|
||||||
|
import { randomTxxx } from "../controller/txxx/txxxRandom";
|
||||||
|
|
||||||
// PornHub
|
// PornHub
|
||||||
import { getPornhub } from "../controller/pornhub/pornhubGet";
|
import { getPornhub } from "../controller/pornhub/pornhubGet";
|
||||||
import { searchPornhub } from "../controller/pornhub/pornhubSearch";
|
import { searchPornhub } from "../controller/pornhub/pornhubSearch";
|
||||||
@@ -65,6 +78,14 @@ function scrapeRoutes() {
|
|||||||
router.get("/youporn/search", cors(), slow, limiter, searchYouporn);
|
router.get("/youporn/search", cors(), slow, limiter, searchYouporn);
|
||||||
router.get("/youporn/related", cors(), slow, limiter, relatedYouporn);
|
router.get("/youporn/related", cors(), slow, limiter, relatedYouporn);
|
||||||
router.get("/youporn/random", cors(), slow, limiter, randomYouporn);
|
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;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
97
src/scraper/eporner/epornerGetController.ts
Normal file
97
src/scraper/eporner/epornerGetController.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { load } from "cheerio";
|
||||||
|
import LustPress from "../../LustPress";
|
||||||
|
import { IVideoData } from "../../interfaces";
|
||||||
|
|
||||||
|
const lust = new LustPress();
|
||||||
|
|
||||||
|
function calculateRatingFromStrings(
|
||||||
|
upVote: string,
|
||||||
|
downVote: string
|
||||||
|
): number {
|
||||||
|
const up = parseInt(upVote.replace(/,/g, ""), 10) || 0;
|
||||||
|
const down = parseInt(downVote.replace(/,/g, ""), 10) || 0;
|
||||||
|
|
||||||
|
const total = up + down;
|
||||||
|
if (total === 0) return 0;
|
||||||
|
|
||||||
|
return up / total;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function scrapeContent(url: string) {
|
||||||
|
try {
|
||||||
|
const resolve = await lust.fetchBody(url);
|
||||||
|
const $ = load(resolve);
|
||||||
|
|
||||||
|
class EPorner {
|
||||||
|
link: string;
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
image: string;
|
||||||
|
duration: string;
|
||||||
|
views: string;
|
||||||
|
rating: string;
|
||||||
|
publish: string;
|
||||||
|
upVote: string;
|
||||||
|
downVote: string;
|
||||||
|
video: string;
|
||||||
|
tags: string[];
|
||||||
|
models: string[];
|
||||||
|
constructor() {
|
||||||
|
// https://www.eporner.com/video-ibvqvezXzcs/ivy-seduces-her-dad-s-friend/
|
||||||
|
|
||||||
|
this.link = $("link[rel='canonical']").attr("href") || "None";
|
||||||
|
this.id = this.link.split("/")[3] + "/" + this.link.split("/")[4] || "None";
|
||||||
|
|
||||||
|
this.title = $("meta[property='og:title']").attr("content") || "None";
|
||||||
|
this.image = $("meta[property='og:image']").attr("content") || "None";
|
||||||
|
this.duration = $("meta[property='og:duration']").attr("content") || "0";
|
||||||
|
this.views = $("#cinemaviews1").text().trim() || "0";
|
||||||
|
this.upVote = $(".likeup i").first().text().trim() || "0";
|
||||||
|
this.downVote = $(".likedown i").first().text().trim() || "0";
|
||||||
|
|
||||||
|
const jsonLdText = $("script[type=\"application/ld+json\"]").first().html() || "{}";
|
||||||
|
const jsonLd = JSON.parse(jsonLdText);
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const isoDuration = jsonLd.duration; // "PT00H8M12S"
|
||||||
|
this.publish = jsonLd.uploadDate || "None";
|
||||||
|
|
||||||
|
const ratingValue = calculateRatingFromStrings(this.upVote, this.downVote);
|
||||||
|
this.rating = (ratingValue * 100).toFixed(2);
|
||||||
|
|
||||||
|
const videoPart = this.link.split("/")[3] || "None";
|
||||||
|
const code = videoPart.replace("video-", "");
|
||||||
|
this.video = `https://www.eporner.com/embed/${code}/`;
|
||||||
|
|
||||||
|
this.tags = $("li.vit-category a")
|
||||||
|
.map((_, el) => $(el).text().trim()).get();
|
||||||
|
|
||||||
|
this.models = $("li.vit-pornstar a")
|
||||||
|
.map((_, el) => $(el).text().trim()).get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ep = new EPorner();
|
||||||
|
const data: IVideoData = {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
title: lust.removeHtmlTagWithoutSpace(ep.title),
|
||||||
|
id: ep.id,
|
||||||
|
image: ep.image,
|
||||||
|
duration: lust.secondToMinute(Number(ep.duration)),
|
||||||
|
views: ep.views,
|
||||||
|
rating: ep.rating,
|
||||||
|
uploaded: ep.publish,
|
||||||
|
upvoted: ep.upVote,
|
||||||
|
downvoted: ep.downVote,
|
||||||
|
models: ep.models,
|
||||||
|
tags: ep.tags
|
||||||
|
},
|
||||||
|
source: ep.link,
|
||||||
|
assets: [ep.video, ep.image]
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error;
|
||||||
|
throw Error(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/scraper/eporner/epornerGetRelatedController.ts
Normal file
46
src/scraper/eporner/epornerGetRelatedController.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { load } from "cheerio";
|
||||||
|
import LustPress from "../../LustPress";
|
||||||
|
import { ISearchVideoData } from "../../interfaces";
|
||||||
|
|
||||||
|
const lust = new LustPress();
|
||||||
|
|
||||||
|
export async function scrapeContent(url: string) {
|
||||||
|
const html = await lust.fetchBody(url);
|
||||||
|
const $ = load(html);
|
||||||
|
|
||||||
|
const data = $("#relateddiv .mb")
|
||||||
|
.map((i, el) => {
|
||||||
|
const link = $(el).find(".mbimg a").first().attr("href") || "";
|
||||||
|
const match = link.match(/\/(video-|hd-porn\/)([^/]+)/);
|
||||||
|
let id = "";
|
||||||
|
if (match) {
|
||||||
|
if (match[1] === "video-") {
|
||||||
|
id = `video-${match[2]}`;
|
||||||
|
} else {
|
||||||
|
// hd-porn
|
||||||
|
id = match[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
link: `https://www.eporner.com${link}`,
|
||||||
|
id,
|
||||||
|
title: $(el).find(".mbtit a").text().trim(),
|
||||||
|
image:
|
||||||
|
$(el).find("img").attr("data-src") || $(el).find("img").attr("src"),
|
||||||
|
duration: $(el).find(".mbtim").text().trim(),
|
||||||
|
rating: $(el).find(".mbrate").text().trim(),
|
||||||
|
views: $(el).find(".mbvie").text().trim(),
|
||||||
|
uploader: $(el).find(".mb-uploader a").text().trim(),
|
||||||
|
video: `https://www.eporner.com/embed/${id}`,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.get()
|
||||||
|
.filter((v) => v.id && v.image);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: data as unknown as string[],
|
||||||
|
source: url,
|
||||||
|
} as ISearchVideoData;
|
||||||
|
}
|
||||||
70
src/scraper/eporner/epornerSearchController.ts
Normal file
70
src/scraper/eporner/epornerSearchController.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { load } from "cheerio";
|
||||||
|
import LustPress from "../../LustPress";
|
||||||
|
import { ISearchItem } from "../../interfaces";
|
||||||
|
|
||||||
|
const lust = new LustPress();
|
||||||
|
|
||||||
|
export async function scrapeContent(url: string) {
|
||||||
|
try {
|
||||||
|
const res = await lust.fetchBody(url);
|
||||||
|
const $ = load(res);
|
||||||
|
|
||||||
|
class EPornerSearch {
|
||||||
|
data: ISearchItem[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.data = $("#vidresults .mb")
|
||||||
|
.map((i, el) => {
|
||||||
|
const link =
|
||||||
|
$(el).find(".mbimg a").first().attr("href") || "";
|
||||||
|
|
||||||
|
const match = link.match(/\/(video-|hd-porn\/)([^/]+)/);
|
||||||
|
|
||||||
|
let id = "";
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
if (match[1] === "video-") {
|
||||||
|
id = `video-${match[2]}`;
|
||||||
|
} else {
|
||||||
|
id = match[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = id.startsWith("video-")
|
||||||
|
? id.replace("video-", "")
|
||||||
|
: id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
link: `https://www.eporner.com${link}`,
|
||||||
|
id,
|
||||||
|
title: $(el).find(".mbtit a").text().trim(),
|
||||||
|
image: $(el).find(".mbimg img").attr("src"),
|
||||||
|
duration: $(el).find(".mbtim").text().trim(),
|
||||||
|
rating: $(el).find(".mbrate").text().trim(),
|
||||||
|
views: $(el).find(".mbvie").text().trim(),
|
||||||
|
uploader: $(el)
|
||||||
|
.find(".mb-uploader a")
|
||||||
|
.text()
|
||||||
|
.trim(),
|
||||||
|
video: code
|
||||||
|
? `https://www.eporner.com/embed/${code}/`
|
||||||
|
: "None",
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ep = new EPornerSearch();
|
||||||
|
if (ep.data.length === 0) throw Error("No result found");
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: ep.data as ISearchItem[],
|
||||||
|
source: url,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error;
|
||||||
|
throw Error(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ export async function scrapeContent(url: string) {
|
|||||||
const resolve = await lust.fetchBody(url);
|
const resolve = await lust.fetchBody(url);
|
||||||
const $ = load(resolve);
|
const $ = load(resolve);
|
||||||
|
|
||||||
class PornHub {
|
class PornHub {
|
||||||
link: string;
|
link: string;
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -29,28 +29,29 @@ export async function scrapeContent(url: string) {
|
|||||||
this.title = $("meta[property='og:title']").attr("content") || "None";
|
this.title = $("meta[property='og:title']").attr("content") || "None";
|
||||||
this.image = $("meta[property='og:image']").attr("content") || "None";
|
this.image = $("meta[property='og:image']").attr("content") || "None";
|
||||||
//get <meta property="video:duration" content="
|
//get <meta property="video:duration" content="
|
||||||
this.duration = $("meta[property='video:duration']").attr("content") || "0";
|
this.duration =
|
||||||
|
$("meta[property='video:duration']").attr("content") || "0";
|
||||||
this.views = $("div.views > span.count").text() || "None";
|
this.views = $("div.views > span.count").text() || "None";
|
||||||
this.rating = $("div.ratingPercent > span.percent").text() || "None";
|
this.rating = $("div.ratingPercent > span.percent").text() || "None";
|
||||||
this.videoInfo = $("div.videoInfo").text() || "None";
|
this.videoInfo = $("div.videoInfo").text() || "None";
|
||||||
this.upVote = $("span.votesUp").attr("data-rating") || "None";
|
this.upVote = $("span.votesUp").attr("data-rating") || "None";
|
||||||
this.downVote = $("span.votesDown").attr("data-rating") || "None";
|
this.downVote = $("span.votesDown").attr("data-rating") || "None";
|
||||||
this.video = $("meta[property='og:video:url']").attr("content") || "None";
|
this.video =
|
||||||
|
$("meta[property='og:video:url']").attr("content") || "None";
|
||||||
this.tags = $("div.video-info-row")
|
this.tags = $("div.video-info-row")
|
||||||
.find("a")
|
.find("a")
|
||||||
.map((i, el) => {
|
.map((i, el) => {
|
||||||
return $(el).text();
|
return $(el).text();
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
this.tags.shift();
|
this.tags.shift();
|
||||||
this.tags = this.tags.map((el) => lust.removeHtmlTagWithoutSpace(el));
|
this.tags = this.tags.map((el) => lust.removeHtmlTagWithoutSpace(el));
|
||||||
this.models = $("div.pornstarsWrapper.js-pornstarsWrapper")
|
this.models = $("div.pornstarsWrapper.js-pornstarsWrapper a")
|
||||||
.find("a")
|
.map((i, el) => $(el).text().trim())
|
||||||
.map((i, el) => {
|
.get();
|
||||||
return $(el).attr("data-mxptext");
|
|
||||||
}).get();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ph = new PornHub();
|
const ph = new PornHub();
|
||||||
const data: IVideoData = {
|
const data: IVideoData = {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -65,14 +66,14 @@ export async function scrapeContent(url: string) {
|
|||||||
upvoted: ph.upVote,
|
upvoted: ph.upVote,
|
||||||
downvoted: ph.downVote,
|
downvoted: ph.downVote,
|
||||||
models: ph.models,
|
models: ph.models,
|
||||||
tags: ph.tags.filter((el) => el !== "Suggest" && el !== " Suggest")
|
tags: ph.tags.filter((el) => el !== "Suggest" && el !== " Suggest"),
|
||||||
},
|
},
|
||||||
source: ph.link,
|
source: ph.link,
|
||||||
assets: [ph.video, ph.image]
|
assets: [ph.video, ph.image],
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as Error;
|
const e = err as Error;
|
||||||
throw Error(e.message);
|
throw Error(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,78 +6,106 @@ const lust = new LustPress();
|
|||||||
|
|
||||||
export async function scrapeContent(url: string) {
|
export async function scrapeContent(url: string) {
|
||||||
try {
|
try {
|
||||||
const resolve = await lust.fetchBody(url);
|
const buffer = await lust.fetchBody(url);
|
||||||
const $ = load(resolve);
|
const $ = load(buffer.toString("utf8"));
|
||||||
|
|
||||||
class Xhamster {
|
const raw = $("#initials-script").html();
|
||||||
|
const initials = raw
|
||||||
|
? JSON.parse(
|
||||||
|
raw.replace(/^window\.initials\s*=\s*/, "").replace(/;$/, ""),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
class Xhamster {
|
||||||
link: string;
|
link: string;
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
image: string;
|
image: string;
|
||||||
duration: any;
|
duration: string;
|
||||||
views: string;
|
views: string;
|
||||||
rating: string;
|
rating: string;
|
||||||
publish: string;
|
|
||||||
upVote: string;
|
upVote: string;
|
||||||
downVote: string;
|
downVote: string;
|
||||||
video: string;
|
publish: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
models: string[];
|
models: string[];
|
||||||
|
video: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.link = $("link[rel='canonical']").attr("href") || "None";
|
this.link = $("link[rel='canonical']").attr("href") || "None";
|
||||||
this.id = this.link.split("/")[3] + "/" + this.link.split("/")[4] || "None";
|
this.id = this.link.split("/")[4] || "None";
|
||||||
this.title = $("meta[property='og:title']").attr("content") || "None";
|
this.title = $("meta[property='og:title']").attr("content") || "None";
|
||||||
this.image = $("meta[property='og:image']").attr("content") || "None";
|
this.image = $("meta[property='og:image']").attr("content") || "None";
|
||||||
this.duration = $("script#initials-script").html() || "None";
|
|
||||||
//remove window.initials={ and };
|
// defaults
|
||||||
this.duration = this.duration.replace("window.initials=", "");
|
this.duration = "None";
|
||||||
this.duration = this.duration.replace(/;/g, "");
|
this.views = "None";
|
||||||
this.duration = JSON.parse(this.duration);
|
|
||||||
this.duration = this.duration.videoModel.duration || "None";
|
const scripts = $("script")
|
||||||
this.views = $("div.header-icons").find("span").first().text() || "None";
|
.map((i, el) => $(el).html())
|
||||||
this.rating = $("div.header-icons").find("span").eq(1).text() || "None";
|
.get()
|
||||||
this.publish = $("div.entity-info-container__date").attr("data-tooltip") || "None";
|
.filter(Boolean);
|
||||||
this.upVote = $("div.rb-new__info").text().split("/")[0].trim() || "None";
|
|
||||||
this.downVote = $("div.rb-new__info").text().split("/")[1].trim() || "None";
|
const videoScript = scripts.find(
|
||||||
this.video = "https://xheve2.com/embed/" + this.link.split("-").pop() || "None";
|
(s) => s.includes("\"videoModel\"") && s.includes("\"duration\""),
|
||||||
this.tags = $("a.video-tag")
|
);
|
||||||
.map((i, el) => {
|
|
||||||
return $(el).text();
|
if (videoScript) {
|
||||||
}).get();
|
const durMatch = videoScript.match(/"duration"\s*:\s*(\d+)/);
|
||||||
this.tags = this.tags.map((el) => lust.removeHtmlTagWithoutSpace(el));
|
if (durMatch) this.duration = durMatch[1];
|
||||||
this.models = $("a.video-tag")
|
|
||||||
.map((i, el) => {
|
const viewMatch = videoScript.match(/"views"\s*:\s*(\d+)/);
|
||||||
return $(el).attr("href");
|
if (viewMatch) this.views = viewMatch[1];
|
||||||
}
|
}
|
||||||
).get();
|
|
||||||
this.models = this.models.filter((el) => el.startsWith("https://xheve2.com/pornstars/"));
|
this.rating =
|
||||||
this.models = this.models.map((el) => el.replace("https://xheve2.com/pornstars/", ""));
|
initials?.ratingComponent?.ratingModel?.value?.toString() || "None";
|
||||||
|
this.upVote =
|
||||||
|
initials?.ratingComponent?.ratingModel?.likes?.toString() || "None";
|
||||||
|
this.downVote =
|
||||||
|
initials?.ratingComponent?.ratingModel?.dislikes?.toString() ||
|
||||||
|
"None";
|
||||||
|
|
||||||
|
this.publish =
|
||||||
|
$("div.entity-info-container__date").attr("data-tooltip") || "None";
|
||||||
|
|
||||||
|
this.tags =
|
||||||
|
initials?.videoTagsComponent?.tags
|
||||||
|
?.filter((t: any) => t.isTag)
|
||||||
|
.map((t: any) => t.name) || [];
|
||||||
|
|
||||||
|
this.models =
|
||||||
|
initials?.videoTagsComponent?.tags
|
||||||
|
?.filter((t: any) => t.isPornstar)
|
||||||
|
.map((t: any) => t.name) || [];
|
||||||
|
|
||||||
|
const embedId = this.link.split("-").pop()?.replace("/", "");
|
||||||
|
this.video = embedId ? `https://xhamster.com/embed/${embedId}` : "None";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const xh = new Xhamster();
|
const xh = new Xhamster();
|
||||||
const data: IVideoData = {
|
|
||||||
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
title: lust.removeHtmlTagWithoutSpace(xh.title),
|
title: lust.removeHtmlTagWithoutSpace(xh.title),
|
||||||
id: xh.id,
|
id: xh.id,
|
||||||
image: xh.image,
|
image: xh.image,
|
||||||
duration: lust.secondToMinute(Number(xh.duration)),
|
duration: xh.duration,
|
||||||
views: xh.views,
|
views: xh.views,
|
||||||
rating: xh.rating,
|
rating: xh.rating,
|
||||||
uploaded: xh.publish,
|
uploaded: xh.publish,
|
||||||
upvoted: xh.upVote,
|
upvoted: xh.upVote,
|
||||||
downvoted: xh.downVote,
|
downvoted: xh.downVote,
|
||||||
models: xh.models,
|
models: xh.models,
|
||||||
tags: xh.tags
|
tags: xh.tags,
|
||||||
},
|
},
|
||||||
source: xh.link,
|
source: xh.link,
|
||||||
assets: [xh.video, xh.image]
|
assets: [xh.video, xh.image],
|
||||||
};
|
} satisfies IVideoData;
|
||||||
return data;
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as Error;
|
const e = err as Error;
|
||||||
throw Error(e.message);
|
throw new Error(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { load } from "cheerio";
|
import { load } from "cheerio";
|
||||||
import LustPress from "../../LustPress";
|
import LustPress from "../../LustPress";
|
||||||
|
import c from "../../utils/options";
|
||||||
import { ISearchVideoData } from "../../interfaces";
|
import { ISearchVideoData } from "../../interfaces";
|
||||||
|
|
||||||
const lust = new LustPress();
|
const lust = new LustPress();
|
||||||
@@ -16,29 +17,32 @@ export async function scrapeContent(url: string) {
|
|||||||
.map((i, el) => {
|
.map((i, el) => {
|
||||||
const views = $(el).text();
|
const views = $(el).text();
|
||||||
return views;
|
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();
|
const duration = $(el).text();
|
||||||
return duration;
|
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("/")[3] + "/" + link?.split("/")[4],
|
id: link?.split("/")[4],
|
||||||
title: $(el).find("img").attr("alt"),
|
title: $(el).find("img").attr("alt"),
|
||||||
image: $(el).find("img").attr("src"),
|
image: $(el).find("img").attr("src"),
|
||||||
duration: duration[i],
|
duration: duration[i],
|
||||||
views: views[i],
|
views: views[i],
|
||||||
video: $(el).attr("data-previewvideo"),
|
video: `${c.XHAMSTER}/embed/${link?.split("-").pop()}`,
|
||||||
};
|
};
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const xh = new XhamsterSearch();
|
const xh = new XhamsterSearch();
|
||||||
if (xh.search.length === 0) throw Error("No result found");
|
if (xh.search.length === 0) throw Error("No result found");
|
||||||
const data = xh.search as unknown as string[];
|
const data = xh.search as unknown as string[];
|
||||||
@@ -48,7 +52,6 @@ export async function scrapeContent(url: string) {
|
|||||||
source: url,
|
source: url,
|
||||||
};
|
};
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as Error;
|
const e = err as Error;
|
||||||
throw Error(e.message);
|
throw Error(e.message);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ const lust = new LustPress();
|
|||||||
|
|
||||||
export async function scrapeContent(url: string) {
|
export async function scrapeContent(url: string) {
|
||||||
try {
|
try {
|
||||||
console.log(url);
|
|
||||||
const resolve = await lust.fetchBody(url);
|
const resolve = await lust.fetchBody(url);
|
||||||
const $ = load(resolve);
|
const $ = load(resolve);
|
||||||
|
|
||||||
@@ -27,6 +26,7 @@ export async function scrapeContent(url: string) {
|
|||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
bigimg: string;
|
bigimg: string;
|
||||||
video: string;
|
video: string;
|
||||||
|
embed: string;
|
||||||
constructor() {
|
constructor() {
|
||||||
const thumb = $("script")
|
const thumb = $("script")
|
||||||
.map((i, el) => {
|
.map((i, el) => {
|
||||||
@@ -69,6 +69,8 @@ export async function scrapeContent(url: string) {
|
|||||||
.map((i, el) => {
|
.map((i, el) => {
|
||||||
return $(el).text();
|
return $(el).text();
|
||||||
}).get();
|
}).get();
|
||||||
|
this.embed = $("input#copy-video-embed").attr("value") || "None";
|
||||||
|
this.embed = this.embed.split("iframe")[1].split(" ")[1].replace(/src=/g, "").replace(/"/g, "") || "None";
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +93,7 @@ export async function scrapeContent(url: string) {
|
|||||||
tags: x.tags.filter((el) => el !== "Edit tags and models")
|
tags: x.tags.filter((el) => el !== "Edit tags and models")
|
||||||
},
|
},
|
||||||
source: x.link,
|
source: x.link,
|
||||||
assets: lust.removeAllSingleQuoteOnArray([x.thumbnail, x.bigimg, x.video])
|
assets: lust.removeAllSingleQuoteOnArray([x.embed, x.thumbnail, x.bigimg, x.video])
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
|
|
||||||
|
|||||||
@@ -22,15 +22,16 @@ export async function scrapeContent(url: string) {
|
|||||||
//stop and replace everything after the last ];
|
//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));
|
||||||
|
//console.log(actualResult);
|
||||||
const result = actualResult.map((el: any) => {
|
const result = actualResult.map((el: any) => {
|
||||||
return {
|
return {
|
||||||
link: `${c.XNXX}${el.u}`,
|
link: `${c.XNXX}${el.u}`,
|
||||||
id: el.u,
|
id: el.u.slice(1, -1),
|
||||||
title: el.t,
|
title: el.t,
|
||||||
image: el.i,
|
image: el.i,
|
||||||
duration: el.d,
|
duration: el.d,
|
||||||
views: `${el.n}, ${el.r}`,
|
views: `${el.n}, ${el.r}`,
|
||||||
video: null
|
video: `${c.XNXX}/embedframe/${el.id}`
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ export async function scrapeContent(url: string) {
|
|||||||
.map((i, el) => {
|
.map((i, el) => {
|
||||||
return {
|
return {
|
||||||
link: `${c.XNXX}${$(el).find("a").attr("href")}`,
|
link: `${c.XNXX}${$(el).find("a").attr("href")}`,
|
||||||
id: $(el).find("a").attr("href"),
|
// remove first "/" and last "/"
|
||||||
|
id: $(el).find("a").attr("href")?.slice(1, -1),
|
||||||
title: $(el).find("div.thumb-under").text().split("\n")
|
title: $(el).find("div.thumb-under").text().split("\n")
|
||||||
.map((el) => el.trim()).filter((el) => el !== "")[0],
|
.map((el) => el.trim()).filter((el) => el !== "")[0],
|
||||||
image: $(el).find("img").attr("data-src"),
|
image: $(el).find("img").attr("data-src"),
|
||||||
@@ -25,8 +26,7 @@ export async function scrapeContent(url: string) {
|
|||||||
.map((el) => el.trim()).filter((el) => el !== "")[2],
|
.map((el) => el.trim()).filter((el) => el !== "")[2],
|
||||||
rating: $(el).find("div.thumb-under").text().split("\n")
|
rating: $(el).find("div.thumb-under").text().split("\n")
|
||||||
.map((el) => el.trim()).filter((el) => el !== "")[1],
|
.map((el) => el.trim()).filter((el) => el !== "")[1],
|
||||||
video: null
|
video: `${c.XNXX}/embedframe/${$(el).find("img").attr("data-videoid")}`
|
||||||
|
|
||||||
};
|
};
|
||||||
}).get();
|
}).get();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export async function scrapeContent(url: string) {
|
|||||||
models: string[];
|
models: string[];
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
bigimg: string;
|
bigimg: string;
|
||||||
|
embed: string;
|
||||||
constructor() {
|
constructor() {
|
||||||
this.link = $("meta[property='og:url']").attr("content") || "None";
|
this.link = $("meta[property='og:url']").attr("content") || "None";
|
||||||
this.id = this.link.split("/")[3] + "/" + this.link.split("/")[4] || "None";
|
this.id = this.link.split("/")[3] + "/" + this.link.split("/")[4] || "None";
|
||||||
@@ -60,7 +61,8 @@ export async function scrapeContent(url: string) {
|
|||||||
}
|
}
|
||||||
).get();
|
).get();
|
||||||
this.models = this.models.map((el) => el.split("/")[2]);
|
this.models = this.models.map((el) => el.split("/")[2]);
|
||||||
|
this.embed = $("input#copy-video-embed").attr("value") || "None";
|
||||||
|
this.embed = this.embed.split("iframe")[1].split(" ")[1].replace(/src=/g, "").replace(/"/g, "") || "None";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +83,7 @@ export async function scrapeContent(url: string) {
|
|||||||
tags: xv.tags,
|
tags: xv.tags,
|
||||||
},
|
},
|
||||||
source: xv.link,
|
source: xv.link,
|
||||||
assets: lust.removeAllSingleQuoteOnArray([xv.thumbnail, xv.bigimg, xv.video])
|
assets: lust.removeAllSingleQuoteOnArray([xv.embed, xv.thumbnail, xv.bigimg, xv.video])
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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);
|
||||||
console.log(url);
|
|
||||||
|
|
||||||
class XvideosSearch {
|
class XvideosSearch {
|
||||||
search: object[];
|
search: object[];
|
||||||
@@ -24,12 +23,12 @@ export async function scrapeContent(url: string) {
|
|||||||
const result = actualResult.map((el: any) => {
|
const result = actualResult.map((el: any) => {
|
||||||
return {
|
return {
|
||||||
link: `${c.XVIDEOS}${el.u}`,
|
link: `${c.XVIDEOS}${el.u}`,
|
||||||
id: el.u,
|
id: el.u.slice(1, -1),
|
||||||
title: el.t,
|
title: el.t,
|
||||||
image: el.i,
|
image: el.i,
|
||||||
duration: el.d,
|
duration: el.d,
|
||||||
views: `${el.n}, ${el.r}`,
|
views: `${el.n}, ${el.r}`,
|
||||||
video: null
|
video: `${c.XVIDEOS}/embedframe/${el.id}`
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const lust = new LustPress();
|
|||||||
|
|
||||||
export async function scrapeContent(url: string) {
|
export async function scrapeContent(url: string) {
|
||||||
try {
|
try {
|
||||||
console.log(url);
|
|
||||||
const res = await lust.fetchBody(url);
|
const res = await lust.fetchBody(url);
|
||||||
const $ = load(res);
|
const $ = load(res);
|
||||||
|
|
||||||
@@ -36,7 +35,7 @@ export async function scrapeContent(url: string) {
|
|||||||
? ""
|
? ""
|
||||||
: data[i].duration || "None",
|
: data[i].duration || "None",
|
||||||
rating: null,
|
rating: null,
|
||||||
video: null
|
video: `${c.XVIDEOS}/embedframe/${$(el).find("img").attr("data-videoid")}`
|
||||||
};
|
};
|
||||||
}).get();
|
}).get();
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export async function scrapeContent(url: string) {
|
|||||||
const resolve = await lust.fetchBody(url);
|
const resolve = await lust.fetchBody(url);
|
||||||
const $ = load(resolve);
|
const $ = load(resolve);
|
||||||
|
|
||||||
class YouPorn {
|
class YouPorn {
|
||||||
link: string;
|
link: string;
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -19,33 +19,43 @@ export async function scrapeContent(url: string) {
|
|||||||
rating: string;
|
rating: string;
|
||||||
publish: string;
|
publish: string;
|
||||||
upVote: string;
|
upVote: string;
|
||||||
downVote: null;
|
downVote: string;
|
||||||
video: string;
|
video: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
models: string[];
|
models: string[];
|
||||||
constructor() {
|
constructor() {
|
||||||
this.link = $("link[rel='canonical']").attr("href") || "None";
|
this.link = $("link[rel='canonical']").attr("href") || "None";
|
||||||
this.id = this.link.replace("https://www.youporn.com/watch/", "") || "None";
|
this.id =
|
||||||
|
this.link.replace("https://www.youporn.com/watch/", "") || "None";
|
||||||
this.title = $("meta[property='og:title']").attr("content") || "None";
|
this.title = $("meta[property='og:title']").attr("content") || "None";
|
||||||
this.image = $("meta[property='og:image']").attr("content") || "None";
|
this.image = $("meta[property='og:image']").attr("content") || "None";
|
||||||
this.duration = $("meta[property='video:duration']").attr("content") || "0";
|
this.duration =
|
||||||
this.views = $("div.feature.infoValueBlock").find("div[data-value]").attr("data-value") || "0";
|
$("meta[property='video:duration']").attr("content") || "0";
|
||||||
this.rating = $("div.feature").find("span").text().replace(/[^0-9.,%]/g, "") || "0";
|
this.publish =
|
||||||
this.publish = $("div.video-uploaded").find("span").text() || "None";
|
$("span.publishedDate").text().replace("Published on", "").trim() ||
|
||||||
this.upVote = this.views;
|
"None";
|
||||||
this.downVote = null;
|
|
||||||
|
this.upVote = "None";
|
||||||
|
this.downVote = "None";
|
||||||
this.video = `https://www.youporn.com/embed/${this.id}`;
|
this.video = `https://www.youporn.com/embed/${this.id}`;
|
||||||
this.tags = $("a[data-espnode='category_tag'], a[data-espnode='porntag_tag']")
|
this.views = $("span.tm_infoValue").first().text() || "0";
|
||||||
.map((i, el) => {
|
this.rating = $("span.tm_rating_percent").text() || "0";
|
||||||
return $(el).text();
|
|
||||||
}).get();
|
this.tags = $(
|
||||||
this.models = $("a[data-espnode='pornstar_tag']")
|
"div.js_scrollableContent a.bubble-porntag, \
|
||||||
.map((i, el) => {
|
a[data-espnode='category_tag'], \
|
||||||
return $(el).text();
|
a[data-espnode='porntag_tag']",
|
||||||
}).get();
|
)
|
||||||
|
.map((i, el) => $(el).text().trim())
|
||||||
|
.get()
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
this.models = $("#metaDataPornstarInfo a.tm_pornstar_link span")
|
||||||
|
.map((i, el) => $(el).text().replace(",", "").trim())
|
||||||
|
.get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const yp = new YouPorn();
|
const yp = new YouPorn();
|
||||||
const data: IVideoData = {
|
const data: IVideoData = {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -60,14 +70,14 @@ export async function scrapeContent(url: string) {
|
|||||||
upvoted: yp.upVote,
|
upvoted: yp.upVote,
|
||||||
downvoted: yp.downVote,
|
downvoted: yp.downVote,
|
||||||
models: yp.models,
|
models: yp.models,
|
||||||
tags: yp.tags
|
tags: yp.tags,
|
||||||
},
|
},
|
||||||
source: yp.link,
|
source: yp.link,
|
||||||
assets: [yp.video, yp.image]
|
assets: [yp.video, yp.image],
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as Error;
|
const e = err as Error;
|
||||||
throw Error(e.message);
|
throw Error(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,31 +11,74 @@ export async function scrapeContent(url: string) {
|
|||||||
const $ = load(res);
|
const $ = load(res);
|
||||||
|
|
||||||
class YouPornSearch {
|
class YouPornSearch {
|
||||||
dur: string[];
|
links: string[];
|
||||||
|
ids: string[];
|
||||||
|
titles: string[];
|
||||||
|
images: string[];
|
||||||
|
durations: string[];
|
||||||
|
views: string[];
|
||||||
search: object[];
|
search: object[];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.dur = $("div.video-duration").map((i, el) => {
|
const cards = $("div.video-box.pc");
|
||||||
return $(el).text();
|
|
||||||
}).get();
|
this.links = cards
|
||||||
this.search = $("a[href^='/watch/']")
|
|
||||||
.map((i, el) => {
|
.map((i, el) => {
|
||||||
const link = $(el).attr("href");
|
return $(el).find("a.tm_video_link").attr("href");
|
||||||
const id = `${link}`.split("/")[2] + "/" + `${link}`.split("/")[3];
|
})
|
||||||
const title = $(el).find("div.video-box-title").text();
|
.get();
|
||||||
const image = $(el).find("img").attr("data-thumbnail");
|
|
||||||
|
this.ids = this.links.map((link) => link?.split("/")[2]);
|
||||||
|
|
||||||
|
this.titles = cards
|
||||||
|
.map((i, el) => {
|
||||||
|
return $(el).find("a.tm_video_title span").text().trim();
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
|
||||||
|
this.images = cards
|
||||||
|
.map((i, el) => {
|
||||||
|
return (
|
||||||
|
$(el).find("img.thumb-image").attr("data-src") ||
|
||||||
|
$(el).find("img.thumb-image").attr("src")
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
|
||||||
|
this.durations = cards
|
||||||
|
.map((i, el) => {
|
||||||
|
return $(el).find("div.tm_video_duration span").text().trim();
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
|
||||||
|
this.views = cards
|
||||||
|
.map((i, el) => {
|
||||||
|
return (
|
||||||
|
$(el)
|
||||||
|
.find(".view-rating-container .info-views")
|
||||||
|
.first()
|
||||||
|
.text()
|
||||||
|
.trim() || "None"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
|
||||||
|
this.search = cards
|
||||||
|
.map((i) => {
|
||||||
return {
|
return {
|
||||||
link: `${c.YOUPORN}${link}`,
|
link: `${c.YOUPORN}${this.links[i]}`,
|
||||||
id: id,
|
id: this.ids[i],
|
||||||
title: lust.removeHtmlTagWithoutSpace(title),
|
title: lust.removeHtmlTagWithoutSpace(this.titles[i]),
|
||||||
image: image,
|
image: this.images[i],
|
||||||
duration: this.dur[i],
|
duration: this.durations[i],
|
||||||
views: null,
|
views: this.views[i],
|
||||||
video: `https://www.youporn.com/embed/${id}`,
|
video: `https://www.youporn.com/embed/${this.ids[i]}`,
|
||||||
};
|
};
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const yp = new YouPornSearch();
|
const yp = new YouPornSearch();
|
||||||
if (yp.search.length === 0) throw Error("No result found");
|
if (yp.search.length === 0) throw Error("No result found");
|
||||||
const data = yp.search as unknown as string[];
|
const data = yp.search as unknown as string[];
|
||||||
@@ -45,9 +88,8 @@ export async function scrapeContent(url: string) {
|
|||||||
source: url,
|
source: url,
|
||||||
};
|
};
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as Error;
|
const e = err as Error;
|
||||||
throw Error(e.message);
|
throw Error(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const limiter = rateLimit({
|
|||||||
const slow = slowDown({
|
const slow = slowDown({
|
||||||
delayAfter: 50,
|
delayAfter: 50,
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
delayMs: 1000,
|
delayMs: () => 1000,
|
||||||
maxDelayMs: 20000,
|
maxDelayMs: 20000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
export default {
|
export default {
|
||||||
|
EPORNER: "https://www.eporner.com",
|
||||||
PORNHUB: "https://www.pornhub.com",
|
PORNHUB: "https://www.pornhub.com",
|
||||||
XNXX: "https://www.xnxx.com",
|
XNXX: "https://www.xnxx.com",
|
||||||
REDTUBE: "https://www.redtube.com",
|
REDTUBE: "https://www.redtube.com",
|
||||||
XVIDEOS: "https://www.xvideos.com",
|
XVIDEOS: "https://www.xvideos.com",
|
||||||
XHAMSTER: "https://xheve2.com",
|
XHAMSTER: "https://xhamster.com/",
|
||||||
YOUPORN: "https://www.youporn.com",
|
YOUPORN: "https://www.youporn.com",
|
||||||
|
TXXX: "https://txxx.com/",
|
||||||
JAVHD: "https://javhd.today"
|
JAVHD: "https://javhd.today"
|
||||||
};
|
};
|
||||||
62
test/lustpress.test.ts
Normal file
62
test/lustpress.test.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import p from "phin";
|
||||||
|
|
||||||
|
const port = process.env.PORT ?? 3000;
|
||||||
|
|
||||||
|
type ApiResponse = {
|
||||||
|
success: boolean;
|
||||||
|
data?: {
|
||||||
|
id?: string | number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
async function run(path: string) {
|
||||||
|
const res = await p({
|
||||||
|
url: `http://localhost:${port}${path}`,
|
||||||
|
parse: "json",
|
||||||
|
timeout: 20000
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(res.statusCode, 200);
|
||||||
|
|
||||||
|
const json = res.body as ApiResponse;
|
||||||
|
|
||||||
|
console.log(JSON.stringify(json, null, 2));
|
||||||
|
|
||||||
|
if (!json.success) {
|
||||||
|
throw new Error("scraper failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("pornhub", async () => {
|
||||||
|
await run("/pornhub/random");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("xnxx", async () => {
|
||||||
|
await run("/xnxx/random");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("redtube", async () => {
|
||||||
|
await run("/redtube/random");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("xvideos", async () => {
|
||||||
|
await run("/xvideos/random");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("xhamster", async () => {
|
||||||
|
await run("/xhamster/random");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("youporn", async () => {
|
||||||
|
await run("/youporn/random");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("eporner", async () => {
|
||||||
|
await run("/eporner/random");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("txxx", async () => {
|
||||||
|
await run("/txxx/random");
|
||||||
|
});
|
||||||
58
test/mock.ts
58
test/mock.ts
@@ -1,20 +1,50 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
import p from "phin";
|
import p from "phin";
|
||||||
|
import { chromium } from "playwright";
|
||||||
import { load } from "cheerio";
|
import { load } from "cheerio";
|
||||||
|
import pkg from "../package.json";
|
||||||
|
|
||||||
const url = "https://www.pornhub.com/view_video.php?viewkey=ph63c4e1dc48fe7";
|
const url = "https://www.pornhub.com/view_video.php?viewkey=697a92abd524b";
|
||||||
|
|
||||||
async function test() {
|
async function getCookies() {
|
||||||
const res = await p({
|
const browser = await chromium.launch({ headless: true });
|
||||||
url: url,
|
const context = await browser.newContext();
|
||||||
"headers": {
|
|
||||||
"User-Agent": process.env.USER_AGENT || "lustpress/1.6.0 Node.js/16.9.1",
|
const page = await context.newPage();
|
||||||
},
|
await page.goto(url, { waitUntil: "domcontentloaded" });
|
||||||
});
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
const $ = load(res.body);
|
const cookies = await context.cookies();
|
||||||
const title = $("meta[property='og:title']").attr("content");
|
await browser.close();
|
||||||
console.log(title);
|
|
||||||
console.log(res.statusCode);
|
return cookies.map(c => `${c.name}=${c.value}`).join("; ");
|
||||||
}
|
}
|
||||||
|
|
||||||
test().catch(console.error);
|
test("pornhub og:title extraction", async () => {
|
||||||
|
|
||||||
|
const cookieHeader = await getCookies();
|
||||||
|
|
||||||
|
const res = await p({
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
"User-Agent": process.env.USER_AGENT || `${pkg.name}/${pkg.version} Node.js/${process.versions.node}`,
|
||||||
|
"Cookie": cookieHeader
|
||||||
|
},
|
||||||
|
followRedirects: true
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Cookie Header:", cookieHeader);
|
||||||
|
console.log("Status:", res.statusCode);
|
||||||
|
|
||||||
|
const html = res.body.toString();
|
||||||
|
// console.log(html.slice(0, 1000));
|
||||||
|
|
||||||
|
const $ = load(html);
|
||||||
|
const title = $("meta[property='og:title']").attr("content");
|
||||||
|
|
||||||
|
console.log("Title:", title);
|
||||||
|
|
||||||
|
assert.ok(title, "og:title should exist");
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@ import c from "../src/utils/options";
|
|||||||
import p from "phin";
|
import p from "phin";
|
||||||
|
|
||||||
for (const url of
|
for (const url of
|
||||||
[c.PORNHUB, c.XNXX, c.REDTUBE, c.XVIDEOS, c.XHAMSTER, c.YOUPORN]) {
|
[c.PORNHUB, c.XNXX, c.REDTUBE, c.XVIDEOS, c.XHAMSTER, c.YOUPORN, c.EPORNER, c.TXXX]) {
|
||||||
p({ url }).then(res => {
|
p({ url }).then(res => {
|
||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
console.log(`${url} is not available, status code: ${res.statusCode}, check the sites or your own user-agent`);
|
console.log(`${url} is not available, status code: ${res.statusCode}, check the sites or your own user-agent`);
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"strictBindCallApply": true,
|
"strictBindCallApply": true,
|
||||||
"strictPropertyInitialization": false,
|
"strictPropertyInitialization": false,
|
||||||
"declaration": true
|
"declaration": true,
|
||||||
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*"
|
"src/**/*"
|
||||||
|
|||||||
Reference in New Issue
Block a user