feat: add Eporner and Txxx along with minor fixes (#16)

Co-authored-by: Pi Patel <pi.patel@gmail.com>
This commit is contained in:
Lame-Fortuna
2026-03-12 16:21:04 +05:30
committed by GitHub
parent ac1a6d82fb
commit 09b49f1649
23 changed files with 984 additions and 130 deletions

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

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

View File

@@ -0,0 +1,75 @@
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";
import LustPress from "../../LustPress";
const lust = new LustPress();
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));
}
}

View File

@@ -0,0 +1,72 @@
import { scrapeContent } from "../../scraper/eporner/epornerSearchController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError, spacer } 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));
}
}

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

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

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

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

View File

@@ -14,23 +14,23 @@ export async function getXhamster(req: Request, res: Response) {
* @apiName Get xhamster
* @apiGroup xhamster
* @apiDescription Get a xhamster video based on id
*
*
* @apiParam {String} id Video ID
*
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
*
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
*
* axios.get("https://lust.scathach.id/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
@@ -38,14 +38,14 @@ export async function getXhamster(req: Request, res: Response) {
* print(await resp.json())
*/
const url = `${c.XHAMSTER}/${id}`;
const url = `${c.XHAMSTER}/videos/${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")
useragent: req.get("User-Agent"),
});
return res.json(data);
} catch (err) {

View File

@@ -10,51 +10,58 @@ const lust = new LustPress();
export async function randomXhamster(req: Request, res: Response) {
try {
/**
* @api {get} /xhamster/random Get random xhamster
* @api {get} /xhamster/random Get random xhamster video
* @apiName Get random 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:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/xhamster/random
*
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
*
* axios.get("https://lust.scathach.id/xhamster/random")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/xhamster/random") as resp:
* print(await resp.json())
*/
const resolve = await lust.fetchBody(`${c.XHAMSTER}/newest`);
const $ = 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 random = Math.floor(Math.random() * search_.length);
const url = c.XHAMSTER + search_[random];
const data = await scrapeContent(url);
const videoLinks = $("div.thumb-list__item[data-video-id]")
.map((_, el) => {
const href = $(el).find("a[data-role='thumb-link']").attr("href");
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({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
useragent: req.get("User-Agent"),
});
return res.json(data);
} catch (err) {
const e = err as Error;