Compare commits
70 Commits
14
.env.example
14
.env.example
|
@ -5,4 +5,16 @@ ENABLE_REDIS=true
|
||||||
# REDIS_URL='redis://redis:6379'
|
# REDIS_URL='redis://redis:6379'
|
||||||
|
|
||||||
# if cache enabled, set redis url for external redis
|
# if cache enabled, set redis url for external redis
|
||||||
REDIS_URL='redis://127.0.0.1:6379'
|
REDIS_URL='redis://127.0.0.1:6379'
|
||||||
|
|
||||||
|
# this key is necessary to encrypt/decrypt urls for server-side stream
|
||||||
|
# if not set, a default value (check const DEFAULT_ENCODING_KEY inside src/constants/encoding.ts ) will be used
|
||||||
|
# please generate a new one with command `pwgen 20 1` (pwgen command needs to be installed)
|
||||||
|
# uncomment variable below and add generated value
|
||||||
|
|
||||||
|
# ENCODING_KEY=''
|
||||||
|
|
||||||
|
# this key can be use to disable specific platforms
|
||||||
|
# please add platforms list (comma separated, eg: 'pornhub,youporn,xnxx')
|
||||||
|
# list of platform values can be found in Platforms enum inside src/meta/settings.ts
|
||||||
|
# DISABLED_PLATFORMS=''
|
19
Dockerfile
19
Dockerfile
|
@ -1,21 +1,28 @@
|
||||||
# PHASE 1: copy and build
|
# PHASE 1: copy and build
|
||||||
|
|
||||||
FROM node:22.0.0-alpine3.19 AS build
|
FROM node:22-alpine AS build
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
ENV NEXT_PRIVATE_STANDALONE true
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN rm -rf node_modules && npm i --package-lock-only && npm ci
|
||||||
RUN rm -rf node_modules && npm install && npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# PHASE 2: prepare for exec
|
# PHASE 2: prepare for exec
|
||||||
|
|
||||||
FROM node:22.0.0-alpine3.19 AS exec
|
FROM node:22-alpine AS exec
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
ENV NODE_ENV production
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=build /app/. .
|
COPY --from=build /app/.next/standalone .
|
||||||
|
COPY --from=build /app/.next/static ./.next/static
|
||||||
|
COPY --from=build /app/public ./public
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD ["npm", "run", "start"]
|
ENTRYPOINT [ "node" ]
|
||||||
|
CMD ["server.js"]
|
63
README.md
63
README.md
|
@ -6,7 +6,14 @@ Proxy Raye is an alternative front-end for adult websites. Watch videos on a cle
|
||||||
|
|
||||||
- XVideos
|
- XVideos
|
||||||
- XNXX
|
- XNXX
|
||||||
- (...more coming soon!)
|
- PornHub (experimental)
|
||||||
|
- YouPorn
|
||||||
|
- RedTube
|
||||||
|
- XHamster
|
||||||
|
|
||||||
|
### How to switch between platforms
|
||||||
|
|
||||||
|
Click on settings icon (gear icon on top-right corner). A pop-up menu will let you choose platform and orientation.
|
||||||
|
|
||||||
## Working demos
|
## Working demos
|
||||||
|
|
||||||
|
@ -14,13 +21,39 @@ Vercel hosted demo can be found [here](https://proxyraye.vercel.app).
|
||||||
|
|
||||||
Self-hosted demo can be found [here](https://proxyraye.copyriot.xyz).
|
Self-hosted demo can be found [here](https://proxyraye.copyriot.xyz).
|
||||||
|
|
||||||
|
# Roadmap
|
||||||
|
|
||||||
|
This is the list of features that will be implemented in the future:
|
||||||
|
|
||||||
|
- [ ] Search pagination
|
||||||
|
- [ ] Search filters
|
||||||
|
- [ ] Add support for *bellesa.co*
|
||||||
|
- [ ] Add support for *ohentai.org*
|
||||||
|
- [ ] Add support for *iamsissy.com*
|
||||||
|
- [ ] Video results API
|
||||||
|
- [ ] Favourite videos
|
||||||
|
- [ ] Embed videos
|
||||||
|
- [ ] Download videos
|
||||||
|
- [ ] Share link button for videos
|
||||||
|
|
||||||
|
|
||||||
# Quickstart
|
# Quickstart
|
||||||
|
|
||||||
You can run the project on local by cloning the repo.
|
You can run the project on local by cloning the repo.
|
||||||
|
|
||||||
|
## IMPORTANT: encoding key generation:
|
||||||
|
|
||||||
|
Since version `0.4.0` server-side video streaming is supported and mandatory for some platforms (like PornHub) in order to work properly. In order to avoid random video url injection, urls get encrypted/decrypted by using an encoding key.
|
||||||
|
|
||||||
|
For security reasons it's better to generate a new encoding key. It can be done via console/terminal by running `pwgen 20 1` command. Make sure `pwgen` command is installed. This will generate an alphanumeric string.
|
||||||
|
|
||||||
|
Paste the string to `ENCODING_KEY` environment variable inside `docker-compose.yaml` if you are using Docker, or inside `.env` file if you run the project with npm. See detailed instructions below.
|
||||||
|
|
||||||
|
In case variable is not set, a default encoding key will be used (not recommended!).
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
You can run it via Docker with docker-compose by opening root folder via console and running:
|
You can run project via Docker with docker-compose by opening root folder via console and running:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
@ -36,6 +69,10 @@ A base Redis image will be added to the network.
|
||||||
|
|
||||||
However, Proxy Raye can still work without Redis by setting `ENABLE_REDIS=false` under `environment:`.
|
However, Proxy Raye can still work without Redis by setting `ENABLE_REDIS=false` under `environment:`.
|
||||||
|
|
||||||
|
### Encoding urls
|
||||||
|
|
||||||
|
Please uncomment `ENCODING_KEY=` related line inside `docker-compose.yaml` (under `environment:`) and set value to the string obtained by running `pwgen 20 1`.
|
||||||
|
|
||||||
## Node.js
|
## Node.js
|
||||||
|
|
||||||
You can also run project outside Docker via npm (tested with NodeJS `20.11` and above).
|
You can also run project outside Docker via npm (tested with NodeJS `20.11` and above).
|
||||||
|
@ -50,6 +87,11 @@ npm run start
|
||||||
|
|
||||||
And head browser to `localhost:3000`.
|
And head browser to `localhost:3000`.
|
||||||
|
|
||||||
|
### Encoding urls
|
||||||
|
|
||||||
|
Please rename `.env.example` to `.env` file inside root folder.
|
||||||
|
|
||||||
|
Please uncomment `ENCODING_KEY=` related line inside `.env` file and set value to the string obtained by running `pwgen 20 1`.
|
||||||
|
|
||||||
### (optional) Enable caching
|
### (optional) Enable caching
|
||||||
|
|
||||||
|
@ -61,7 +103,6 @@ REDIS_URL='redis://127.0.0.1:6379'
|
||||||
```
|
```
|
||||||
These values assume a basic Redis instance running on local machine. If your local setup is different, or your Redis instance is somewhere else, please change `REDIS_URL` accordingly.
|
These values assume a basic Redis instance running on local machine. If your local setup is different, or your Redis instance is somewhere else, please change `REDIS_URL` accordingly.
|
||||||
|
|
||||||
|
|
||||||
# Modify
|
# Modify
|
||||||
If you want to edit the project you can start development mode by opening root folder via console and running:
|
If you want to edit the project you can start development mode by opening root folder via console and running:
|
||||||
|
|
||||||
|
@ -78,6 +119,7 @@ The project uses following tech stack:
|
||||||
- Next/Intl
|
- Next/Intl
|
||||||
|
|
||||||
It scrapes data server-side and return treated data to the frontend to be rendered.
|
It scrapes data server-side and return treated data to the frontend to be rendered.
|
||||||
|
|
||||||
# Deploy
|
# Deploy
|
||||||
|
|
||||||
## Vercel
|
## Vercel
|
||||||
|
@ -87,4 +129,17 @@ You can deploy the app on Vercel by cloning this repo on your GitHub/Gitlab and
|
||||||
Due to Vercel's *serverless* nature (which makes every request to XVideos and other platforms come from a different IP) it becomes very hard for *web application firewalls* to ban addresses effectively.
|
Due to Vercel's *serverless* nature (which makes every request to XVideos and other platforms come from a different IP) it becomes very hard for *web application firewalls* to ban addresses effectively.
|
||||||
|
|
||||||
## Self-host
|
## Self-host
|
||||||
You can self host the project on your local server via docker-compose and reverse-proxy exposed port to nginx.
|
|
||||||
|
You can self host the project on your local server via docker-compose and reverse-proxy exposed port to nginx.
|
||||||
|
|
||||||
|
# Disabling platforms
|
||||||
|
|
||||||
|
For several reason you might want to disable some platforms. You can do it by adding `DISABLED_PLATFORMS` environment variable.
|
||||||
|
|
||||||
|
List of platform values can be found in Platforms enum inside `src/meta/settings.ts`
|
||||||
|
|
||||||
|
Please add platforms list comma separated. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
DISABLED_PLATFORMS='pornhub, xnxx'
|
||||||
|
```
|
||||||
|
|
|
@ -23,6 +23,8 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- ENABLE_REDIS=true
|
- ENABLE_REDIS=true
|
||||||
- REDIS_URL=redis://redis:6379
|
- REDIS_URL=redis://redis:6379
|
||||||
|
# Please generate a new encoding key with command `pwgen 20 1`, decomment following variable and insert result into it:
|
||||||
|
# - ENCODING_KEY=
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
|
|
|
@ -2,13 +2,7 @@
|
||||||
"Header": {
|
"Header": {
|
||||||
"title": "Proxy Raye",
|
"title": "Proxy Raye",
|
||||||
"description": "A proxy for porn websites",
|
"description": "A proxy for porn websites",
|
||||||
"disclaimer_0": "Genital sexuality is only one of the many possible conceptions of sexuality",
|
"disclaimer_pornhub": "Warning: PornHub support is experimental. If video player freezes, try reloading the page after a few seconds."
|
||||||
"disclaimer_1": "Platform capitalism makes money on desire flow. Proxies avoid this to happen.",
|
|
||||||
"disclaimer_2": "Platform capitalism is narcissism-driven",
|
|
||||||
"disclaimer_3": "Pornhub annoying elements (like ads) are put there intentionally to make you upgrade to premium",
|
|
||||||
"disclaimer_4": "You're going to masturbate on someone else's imaginary",
|
|
||||||
"disclaimer_5": "No banners or annoying popups. You can jerk off with no hassle!",
|
|
||||||
"disclaimer_6": "You're choosing image over imagination. What if they're not in antithesis?"
|
|
||||||
},
|
},
|
||||||
"NotFound": {
|
"NotFound": {
|
||||||
"uh_oh": "Uh Oh...",
|
"uh_oh": "Uh Oh...",
|
||||||
|
@ -29,7 +23,7 @@
|
||||||
},
|
},
|
||||||
"Results": {
|
"Results": {
|
||||||
"query": "Search results for: {{ query }}",
|
"query": "Search results for: {{ query }}",
|
||||||
"toggle": "Show preview",
|
"toggle": "Toggle opacity",
|
||||||
"noData": "No videos found :("
|
"noData": "No videos found :("
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,13 +2,7 @@
|
||||||
"Header": {
|
"Header": {
|
||||||
"title": "Proxy Raye",
|
"title": "Proxy Raye",
|
||||||
"description": "Un proxy per i siti porno",
|
"description": "Un proxy per i siti porno",
|
||||||
"disclaimer_0": "Quella genitale è solo una delle possibili concezioni della sessualità.",
|
"disclaimer_pornhub": "Attenzione: il supporto per PornHub è sperimentale. Se il player si blocca, provare a ricaricare la pagina dopo qualche secondo."
|
||||||
"disclaimer_1": "Le piattaforme monetizzano i flussi di desiderio. I proxy impediscono che questo accada.",
|
|
||||||
"disclaimer_2": "Le piattaforme si alimentano del narcisisismo degli utenti.",
|
|
||||||
"disclaimer_3": "Gli elementi di disturbo su PornHub sono messi lì a posta per farti passare alla versione Premium.",
|
|
||||||
"disclaimer_4": "Stai per masturbarti sull'immaginario di qualcun altro.",
|
|
||||||
"disclaimer_5": "Niente banner o popup fastidiosi. Puoi masturbarti in santa pace.",
|
|
||||||
"disclaimer_6": "Stai preferendo l'immagine all'immaginazione. E se immagine e immaginazione non fossero in antitesi?"
|
|
||||||
},
|
},
|
||||||
"NotFound": {
|
"NotFound": {
|
||||||
"uh_oh": "Uh Oh...",
|
"uh_oh": "Uh Oh...",
|
||||||
|
@ -29,7 +23,7 @@
|
||||||
},
|
},
|
||||||
"Results": {
|
"Results": {
|
||||||
"query": "Risultati della ricerca per: {{ query }}",
|
"query": "Risultati della ricerca per: {{ query }}",
|
||||||
"toggle": "Mostra anteprime risultati",
|
"toggle": "Attiva/disattiva opacità",
|
||||||
"noData": "Nessun video trovato :("
|
"noData": "Nessun video trovato :("
|
||||||
}
|
}
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "proxyraye-next",
|
"name": "proxyraye-next",
|
||||||
"version": "0.3.0",
|
"version": "0.4.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
@ -17,6 +17,7 @@
|
||||||
"next": "14.2.2",
|
"next": "14.2.2",
|
||||||
"next-intl": "3.11.3",
|
"next-intl": "3.11.3",
|
||||||
"next-nprogress-bar": "2.3.11",
|
"next-nprogress-bar": "2.3.11",
|
||||||
|
"plyr-react": "5.3.0",
|
||||||
"react": "18.3.0",
|
"react": "18.3.0",
|
||||||
"react-cookie": "7.1.4",
|
"react-cookie": "7.1.4",
|
||||||
"react-dom": "18.3.0",
|
"react-dom": "18.3.0",
|
||||||
|
@ -25,7 +26,7 @@
|
||||||
"react-redux": "9.1.1",
|
"react-redux": "9.1.1",
|
||||||
"redis": "4.6.14",
|
"redis": "4.6.14",
|
||||||
"redux-persist": "6.0.0",
|
"redux-persist": "6.0.0",
|
||||||
"video.js": "8.10.0",
|
"video.js": "8.12.0",
|
||||||
"videojs-hls-quality-selector": "2.0.0"
|
"videojs-hls-quality-selector": "2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -24,13 +24,11 @@ export default async function VideoPage({ params }: { params: { platform: Platfo
|
||||||
|
|
||||||
const [data, related] = await new VideoAgent(platform).getVideo(decodedId)
|
const [data, related] = await new VideoAgent(platform).getVideo(decodedId)
|
||||||
|
|
||||||
//const [data, related] = await fetchVideoData(decodedId)
|
if (!data.hlsUrl && (!data.srcSet || data.srcSet.length == 0)) {
|
||||||
|
|
||||||
if (!data.lowResUrl) {
|
|
||||||
redirect(`/${locale}/404`)
|
redirect(`/${locale}/404`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Layout>
|
return <Layout>
|
||||||
<Video id={id} data={data} related={related}/>
|
<Video platform={platform} id={id} data={data} related={related}/>
|
||||||
</Layout>
|
</Layout>
|
||||||
}
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { Platforms } from "@/meta/settings";
|
||||||
|
import { decodeUrl } from "@/utils/string";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
type GetParams = {
|
||||||
|
params: {
|
||||||
|
platform: Platforms
|
||||||
|
encodedUrl: string
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function GET(req: Request, { params }: GetParams) {
|
||||||
|
|
||||||
|
const { platform, encodedUrl } = params;
|
||||||
|
|
||||||
|
if (!Object.keys(Platforms).includes(platform)) {
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Platform not supported!'
|
||||||
|
},
|
||||||
|
{ status: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedUrl = decodeUrl(encodedUrl)
|
||||||
|
|
||||||
|
const response = await axios.get<ReadableStream>(decodedUrl, {
|
||||||
|
responseType: "stream",
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = new Headers();
|
||||||
|
|
||||||
|
headers.set('Content-Type', 'video/mp4');
|
||||||
|
headers.set('Cache-Control', 'no-cache');
|
||||||
|
headers.set('Accept-Ranges', 'bytes');
|
||||||
|
headers.set('Content-Length', response.headers['content-length']);
|
||||||
|
headers.set('Content-Disposition', 'inline');
|
||||||
|
|
||||||
|
return new Response(response.data, {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -3,19 +3,22 @@ import React from 'react';
|
||||||
import style from './Disclaimer.module.scss'
|
import style from './Disclaimer.module.scss'
|
||||||
|
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { Platforms } from '@/meta/settings';
|
||||||
|
|
||||||
const Disclaimer: React.FC = () => {
|
interface Props {
|
||||||
|
platform: Platforms
|
||||||
|
}
|
||||||
|
|
||||||
const MAX_DISCLAIMER_NO = 6
|
const Disclaimer: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const { platform } = props;
|
||||||
|
|
||||||
const t = useTranslations('Header');
|
const t = useTranslations('Header');
|
||||||
|
|
||||||
const getRandomArbitrary = (max: number) => {
|
|
||||||
return Math.floor( Math.random() * max);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.messageBox}>{t(`disclaimer_${getRandomArbitrary(MAX_DISCLAIMER_NO)}`)}</div>
|
<>
|
||||||
|
{platform == Platforms.pornhub && <div className={style.messageBox}>{t(`disclaimer_pornhub`)}</div>}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Cookies, XVideosOrientations } from '@/meta/settings';
|
import { Cookies, OrientationMapper, Platforms, XVideosOrientations } from '@/meta/settings';
|
||||||
|
|
||||||
import css from './Orientation.module.scss'
|
import css from './Orientation.module.scss'
|
||||||
|
|
||||||
|
@ -16,11 +16,17 @@ interface Props {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getOrientations = (platform: Platforms):Object => {
|
||||||
|
return OrientationMapper[platform];
|
||||||
|
}
|
||||||
|
|
||||||
const Orientation: React.FC<Props> = (props) => {
|
const Orientation: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
const { labels, handleClose } = props
|
const { labels, handleClose } = props
|
||||||
|
|
||||||
const [cookies] = useCookies([Cookies.orientation]);
|
const [cookies] = useCookies([Cookies.orientation, Cookies.platform]);
|
||||||
|
|
||||||
|
const orientationsList = cookies.platform ? getOrientations(cookies.platform) : XVideosOrientations
|
||||||
|
|
||||||
const handleChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
|
@ -34,7 +40,7 @@ const Orientation: React.FC<Props> = (props) => {
|
||||||
<div className={css.container}>
|
<div className={css.container}>
|
||||||
<div className={css.title}>{labels.title}</div>
|
<div className={css.title}>{labels.title}</div>
|
||||||
<select defaultValue={ cookies.orientation ?? XVideosOrientations.etero } onChange={handleChange} name={'orientation'} aria-label={labels.title}>
|
<select defaultValue={ cookies.orientation ?? XVideosOrientations.etero } onChange={handleChange} name={'orientation'} aria-label={labels.title}>
|
||||||
{Object.keys(XVideosOrientations).map((elem, key) => {
|
{Object.keys(orientationsList).map((elem, key) => {
|
||||||
return <option className={css.option} key={key} value={elem}>{elem.toUpperCase()}</option>
|
return <option className={css.option} key={key} value={elem}>{elem.toUpperCase()}</option>
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Cookies, Platforms } from '@/meta/settings';
|
import { Cookies, OrientationMapper, Platforms } from '@/meta/settings';
|
||||||
|
|
||||||
import css from './Platform.module.scss'
|
import css from './Platform.module.scss'
|
||||||
|
|
||||||
|
@ -11,30 +11,45 @@ import { useCookies } from 'react-cookie';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
handleClose(): void
|
handleClose(): void
|
||||||
|
enabledPlatforms: string[]
|
||||||
labels: {
|
labels: {
|
||||||
title: string,
|
title: string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mapOrientationToPlatform = (platform: string, orientation: string): string | undefined => {
|
||||||
|
const orientations = OrientationMapper[platform as Platforms]
|
||||||
|
return Object.keys(orientations).includes(orientation) ? orientation : String(Object.keys(orientations)[0])
|
||||||
|
}
|
||||||
|
|
||||||
const Platform: React.FC<Props> = (props) => {
|
const Platform: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
const { labels, handleClose } = props
|
const { labels, handleClose, enabledPlatforms } = props
|
||||||
|
|
||||||
const [cookies] = useCookies([Cookies.platform]);
|
const [cookies] = useCookies([Cookies.platform, Cookies.orientation]);
|
||||||
|
|
||||||
const handleChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
|
|
||||||
await setCookie(Cookies.platform, value)
|
await setCookie(Cookies.platform, value)
|
||||||
|
|
||||||
|
if (cookies.orientation) {
|
||||||
|
const newOrientation = mapOrientationToPlatform(value, cookies.orientation)
|
||||||
|
newOrientation && await setCookie(Cookies.orientation, newOrientation)
|
||||||
|
}
|
||||||
|
|
||||||
handleClose()
|
handleClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={css.container}>
|
<div className={css.container}>
|
||||||
<div className={css.title}>{labels.title}</div>
|
<div className={css.title}>{labels.title}</div>
|
||||||
<select defaultValue={ cookies.platform ?? Platforms.xvideos } onChange={handleChange} name={'platform'} aria-label={labels.title}>
|
<select defaultValue={cookies.platform ?? Platforms.xvideos} onChange={handleChange} name={'platform'} aria-label={labels.title}>
|
||||||
{Object.keys(Platforms).map((elem, key) => {
|
{Object.keys(Platforms).map((elem, key) => {
|
||||||
|
if (!enabledPlatforms.includes(elem)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return <option className={css.option} key={key} value={elem}>{elem.toUpperCase()}</option>
|
return <option className={css.option} key={key} value={elem}>{elem.toUpperCase()}</option>
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import Orientation from './Orientation';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
handleClose(): void
|
handleClose(): void
|
||||||
|
enabledPlatforms: string[]
|
||||||
labels: {
|
labels: {
|
||||||
title: string
|
title: string
|
||||||
platform: any
|
platform: any
|
||||||
|
@ -19,7 +20,7 @@ interface Props {
|
||||||
|
|
||||||
const LangSwitcher: React.FC<Props> = (props) => {
|
const LangSwitcher: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
const { labels, handleClose } = props
|
const { labels, handleClose, enabledPlatforms } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dialog open>
|
<dialog open>
|
||||||
|
@ -29,7 +30,7 @@ const LangSwitcher: React.FC<Props> = (props) => {
|
||||||
<div className={style.close} onClick={() => { handleClose() }}><IoCloseCircleOutline size={24} /></div>
|
<div className={style.close} onClick={() => { handleClose() }}><IoCloseCircleOutline size={24} /></div>
|
||||||
</header>
|
</header>
|
||||||
<div className={style.content}>
|
<div className={style.content}>
|
||||||
<Platform handleClose={handleClose} labels={{ title: labels.platform.title }} />
|
<Platform enabledPlatforms={enabledPlatforms} handleClose={handleClose} labels={{ title: labels.platform.title }} />
|
||||||
<Orientation handleClose={handleClose} labels={{ title: labels.orientation.title }} />
|
<Orientation handleClose={handleClose} labels={{ title: labels.orientation.title }} />
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -9,11 +9,12 @@ import Modal from './Modal';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
labels: any
|
labels: any
|
||||||
|
enabledPlatforms: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const Settings: React.FC<Props> = (props) => {
|
const Settings: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
const { labels } = props
|
const { labels, enabledPlatforms } = props
|
||||||
|
|
||||||
const [showModal, setShowModal] = useState<boolean>(false)
|
const [showModal, setShowModal] = useState<boolean>(false)
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ const Settings: React.FC<Props> = (props) => {
|
||||||
{<IoSettingsOutline size={24} />}
|
{<IoSettingsOutline size={24} />}
|
||||||
</Icon>
|
</Icon>
|
||||||
|
|
||||||
{showModal && <Modal handleClose={() => setShowModal(false)} labels={labels} />}
|
{showModal && <Modal enabledPlatforms={enabledPlatforms} handleClose={() => setShowModal(false)} labels={labels} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,7 @@ import Repo from './Repo';
|
||||||
import Language from './Language';
|
import Language from './Language';
|
||||||
import { LangOption } from '@/meta/settings';
|
import { LangOption } from '@/meta/settings';
|
||||||
import Settings from './Settings';
|
import Settings from './Settings';
|
||||||
|
import { getEnabledPlatforms } from '@/utils/platforms';
|
||||||
|
|
||||||
const Menu: React.FC = () => {
|
const Menu: React.FC = () => {
|
||||||
|
|
||||||
|
@ -34,12 +35,14 @@ const Menu: React.FC = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enabledPlatforms = getEnabledPlatforms()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.container}>
|
<div className={style.container}>
|
||||||
<Repo />
|
<Repo />
|
||||||
<Language labels={languageLabels} />
|
<Language labels={languageLabels} />
|
||||||
<Theme />
|
<Theme />
|
||||||
<Settings labels={settingsLabels} />
|
<Settings enabledPlatforms={enabledPlatforms} labels={settingsLabels} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
@import 'spacing';
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin-bottom: $spacing_32;
|
||||||
|
|
||||||
|
[type="button"],
|
||||||
|
[type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
--plyr-color-main: var(--primary)
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { usePlyr } from 'plyr-react';
|
||||||
|
|
||||||
|
import style from './PlyrJS.module.scss'
|
||||||
|
|
||||||
|
import { VideoData } from '@/meta/data';
|
||||||
|
|
||||||
|
import "plyr-react/plyr.css"
|
||||||
|
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: VideoData
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlyrJS: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const PlyrComponent = React.useMemo(() => {
|
||||||
|
return dynamic(() => import("plyr-react").then(() => {
|
||||||
|
const Comp = React.forwardRef((props, ref) => {
|
||||||
|
//@ts-ignore
|
||||||
|
const { source, options = null, ...rest } = props
|
||||||
|
//@ts-ignore
|
||||||
|
const raptorRef = usePlyr(ref, {
|
||||||
|
source,
|
||||||
|
options,
|
||||||
|
})
|
||||||
|
return <video ref={raptorRef} className="plyr-react plyr" {...rest} />
|
||||||
|
})
|
||||||
|
|
||||||
|
Comp.displayName = 'PlyrComponent'
|
||||||
|
|
||||||
|
return Comp;
|
||||||
|
}), { ssr: false });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
PlyrComponent.displayName = 'PlyrComponent'
|
||||||
|
|
||||||
|
const { data } = props;
|
||||||
|
|
||||||
|
const plyrProps = {
|
||||||
|
source: { type: 'video', sources: data.srcSet }, // https://github.com/sampotts/plyr#the-source-setter
|
||||||
|
options: {
|
||||||
|
controls: [
|
||||||
|
'play-large',
|
||||||
|
'play', // Play/pause playback
|
||||||
|
'progress', // The progress bar and scrubber for playback and buffering
|
||||||
|
'current-time', // The current time of playback
|
||||||
|
'duration', // The full duration of the media
|
||||||
|
'mute', // Toggle mute
|
||||||
|
'volume', // Volume control
|
||||||
|
'captions', // Toggle captions
|
||||||
|
'settings', // Settings menu
|
||||||
|
'fullscreen', // Toggle fullscreen
|
||||||
|
]
|
||||||
|
}, // https://github.com/sampotts/plyr#options
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.container}>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<PlyrComponent {...plyrProps} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PlyrJS;
|
|
@ -31,7 +31,11 @@ const Thumbnail: React.FC<Props> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(style.thumbnailContainer, { [style.show]: show } )}>
|
<div className={classNames(style.thumbnailContainer, { [style.show]: show } )}>
|
||||||
<Link href={`/${locale}/video/${platform}/${encodedUri}`}>
|
<Link href={`/${locale}/video/${platform}/${encodedUri}`}>
|
||||||
<Img className={style.image} src={imgUrl} unloader={<div className={style.imgPlaceholder}></div>} />
|
<Img
|
||||||
|
className={style.image}
|
||||||
|
src={imgUrl}
|
||||||
|
loader={<div className={style.imgPlaceholder}></div>}
|
||||||
|
unloader={<div className={style.imgPlaceholder}></div>} />
|
||||||
<div className={style.text}>{text}</div>
|
<div className={style.text}>{text}</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@import 'breakpoints';
|
@import 'breakpoints';
|
||||||
|
@import 'fontsize';
|
||||||
|
|
||||||
.toggleContainer {
|
.toggleContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -11,7 +12,7 @@
|
||||||
border-color: var(--primary-focus);
|
border-color: var(--primary-focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: $tablet) {
|
.label {
|
||||||
display: none;
|
font-size: $font-size-medium;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,9 +6,9 @@ import videojs from 'video.js';
|
||||||
|
|
||||||
import 'video.js/dist/video-js.css';
|
import 'video.js/dist/video-js.css';
|
||||||
|
|
||||||
import style from './VideoJS.module.scss'
|
import style from './VJSContent.module.scss'
|
||||||
|
|
||||||
export const VideoJS = (props: { options: any; onReady: any; }) => {
|
export const VJSContent = (props: { options: any; onReady: any; }) => {
|
||||||
const videoRef = React.useRef(null);
|
const videoRef = React.useRef(null);
|
||||||
const playerRef = React.useRef(null);
|
const playerRef = React.useRef(null);
|
||||||
const {options, onReady} = props;
|
const {options, onReady} = props;
|
||||||
|
@ -64,4 +64,4 @@ export const VideoJS = (props: { options: any; onReady: any; }) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default VideoJS;
|
export default VJSContent;
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import style from './Player.module.scss'
|
import style from './VideoJS.module.scss'
|
||||||
|
|
||||||
import VideoJS from './VideoJS';
|
import VJSContent from './VJSContent';
|
||||||
import { VideoData } from '@/meta/data';
|
import { VideoData } from '@/meta/data';
|
||||||
|
|
||||||
import 'videojs-hls-quality-selector';
|
import 'videojs-hls-quality-selector';
|
||||||
|
@ -13,12 +13,12 @@ interface Props {
|
||||||
data: VideoData
|
data: VideoData
|
||||||
}
|
}
|
||||||
|
|
||||||
const Player: React.FC<Props> = (props) => {
|
const VideoJS: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
const { data } = props;
|
const { data } = props;
|
||||||
|
|
||||||
const videoSrc = data.hlsUrl ?? data.lowResUrl
|
const videoSrc = data.hlsUrl
|
||||||
const videoType = data.hlsUrl ? 'application/x-mpegURL' : 'video/mp4'
|
const videoType = 'application/x-mpegURL'
|
||||||
|
|
||||||
const playerRef = React.useRef(null);
|
const playerRef = React.useRef(null);
|
||||||
|
|
||||||
|
@ -55,9 +55,9 @@ const Player: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.container}>
|
<div className={style.container}>
|
||||||
<VideoJS options={videoJsOptions} onReady={handlePlayerReady} />
|
<VJSContent options={videoJsOptions} onReady={handlePlayerReady} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Player;
|
export default VideoJS;
|
|
@ -1,27 +1,34 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Header from '@/components/Layout/Header';
|
import Header from '@/components/Layout/Header';
|
||||||
import Player from '@/components/Layout/Player';
|
|
||||||
|
import VideoJS from '@/components/Layout/VideoJS';
|
||||||
|
import PlyrJS from '@/components/Layout/PlyrJS';
|
||||||
|
|
||||||
import SearchBar from '@/components/Layout/SearchBar';
|
import SearchBar from '@/components/Layout/SearchBar';
|
||||||
import Results from '@/components/Layout/Results';
|
import Results from '@/components/Layout/Results';
|
||||||
|
|
||||||
import { GalleryData, VideoData } from '@/meta/data';
|
import { GalleryData, VideoData } from '@/meta/data';
|
||||||
|
import { Platforms } from '@/meta/settings';
|
||||||
|
import Disclaimer from '@/components/Layout/Header/Disclaimer';
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string
|
id: string
|
||||||
data: VideoData
|
data: VideoData
|
||||||
related: GalleryData[]
|
related: GalleryData[]
|
||||||
|
platform: Platforms
|
||||||
}
|
}
|
||||||
|
|
||||||
const Video: React.FC<Props> = (props) => {
|
const Video: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
const { data, related } = props;
|
const { data, related, platform } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
<Player data={data} />
|
<Disclaimer platform={platform} />
|
||||||
|
{data.hlsUrl ? <VideoJS data={data} /> : <PlyrJS data={data} />}
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
{related && <Results data={related} />}
|
{related && <Results data={related} />}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export const DEFAULT_ENCODING_KEY = 'oom2oz8ut0ieshie1Hae'
|
|
@ -1,4 +1,21 @@
|
||||||
export const DEFAULT_XVIDEOS_CONTENT_EXPIRY = { EX: 60 * 60 * 24 };
|
const EX_MIN = 60
|
||||||
export const DEFAULT_XNXX_CONTENT_EXPIRY = { EX: 60 * 60 * 24 };
|
const EX_HOURLY = 60 * 60
|
||||||
|
const EX_DAILY = 60 * 60 * 24
|
||||||
|
|
||||||
|
export const DEFAULT_PORNHUB_GALLERY_EXPIRY = { EX: EX_HOURLY };
|
||||||
|
export const DEFAULT_PORNHUB_VIDEO_EXPIRY = { EX: EX_MIN };
|
||||||
|
|
||||||
|
export const DEFAULT_XVIDEOS_CONTENT_EXPIRY = { EX: EX_HOURLY };
|
||||||
|
|
||||||
|
export const DEFAULT_XNXX_CONTENT_EXPIRY = { EX: EX_HOURLY };
|
||||||
|
|
||||||
|
export const DEFAULT_YOUPORN_GALLERY_EXPIRY = { EX: EX_HOURLY };
|
||||||
|
export const DEFAULT_YOUPORN_VIDEO_EXPIRY = { EX: EX_HOURLY };
|
||||||
|
|
||||||
|
export const DEFAULT_REDTUBE_GALLERY_EXPIRY = { EX: EX_HOURLY };
|
||||||
|
export const DEFAULT_REDTUBE_VIDEO_EXPIRY = { EX: EX_HOURLY };
|
||||||
|
|
||||||
|
export const DEFAULT_XHAMSTER_GALLERY_EXPIRY = { EX: EX_HOURLY };
|
||||||
|
export const DEFAULT_XHAMSTER_VIDEO_EXPIRY = { EX: EX_HOURLY };
|
||||||
|
|
||||||
export const DEFAULT_RELATED_VIDEO_KEY_PATH = '/related/'
|
export const DEFAULT_RELATED_VIDEO_KEY_PATH = '/related/'
|
|
@ -0,0 +1 @@
|
||||||
|
export const DEFAULT_VIDEO_STREAM_ROUTE_PREFIX = '/api/stream'
|
|
@ -1,11 +1,52 @@
|
||||||
|
// XVIDEOS
|
||||||
|
|
||||||
export const XVIDEOS_BASE_URL: string = "https://www.xvideos.com"
|
export const XVIDEOS_BASE_URL: string = "https://www.xvideos.com"
|
||||||
export const XVIDEOS_BASE_URL_GAY: string = "https://www.xvideos.com/gay"
|
export const XVIDEOS_BASE_URL_GAY: string = "https://www.xvideos.com/gay"
|
||||||
export const XVIDEOS_BASE_URL_TRANS: string = "https://www.xvideos.com/shemale"
|
export const XVIDEOS_BASE_URL_TRANS: string = "https://www.xvideos.com/shemale"
|
||||||
|
|
||||||
|
|
||||||
|
// XNXX
|
||||||
|
|
||||||
export const XNXX_BASE_URL: string = 'https://www.xnxx.com'
|
export const XNXX_BASE_URL: string = 'https://www.xnxx.com'
|
||||||
|
|
||||||
export const XNXX_BASE_URL_ETERO: string = 'https://www.xnxx.com/best'
|
export const XNXX_BASE_URL_ETERO: string = 'https://www.xnxx.com/best'
|
||||||
export const XNXX_BASE_URL_GAY: string = 'https://www.xnxx.com/best-of-gay'
|
export const XNXX_BASE_URL_GAY: string = 'https://www.xnxx.com/best-of-gay'
|
||||||
export const XNXX_BASE_URL_TRANS: string = 'https://www.xnxx.com/best-of-shemale'
|
export const XNXX_BASE_URL_TRANS: string = 'https://www.xnxx.com/best-of-shemale'
|
||||||
|
|
||||||
export const XNXX_BASE_SEARCH: string = 'https://www.xnxx.com/search'
|
export const XNXX_BASE_SEARCH: string = 'https://www.xnxx.com/search'
|
||||||
|
|
||||||
|
// PORNHUB
|
||||||
|
|
||||||
|
export const PORNHUB_BASE_URL: string = 'https://www.pornhub.com'
|
||||||
|
export const PORNHUB_BASE_URL_VIDEO: string = 'https://www.pornhub.com/view_video.php?viewkey='
|
||||||
|
|
||||||
|
export const PORNHUB_BASE_URL_GAY: string = 'https://www.pornhub.com/gayporn'
|
||||||
|
export const PORNHUB_BASE_URL_GAY_SEARCH: string = 'https://www.pornhub.com/gay'
|
||||||
|
|
||||||
|
// YOUPORN
|
||||||
|
|
||||||
|
export const YOUPORN_BASE_URL: string = 'https://www.youporn.com'
|
||||||
|
export const YOUPORN_BASE_URL_VIDEO: string = 'https://www.youporn.com/watch'
|
||||||
|
|
||||||
|
export const YOUPORN_BASE_SEARCH: string = 'https://www.youporn.com/search/?search-btn=&query='
|
||||||
|
|
||||||
|
// REDTUBE
|
||||||
|
|
||||||
|
export const REDTUBE_BASE_URL: string = 'https://www.redtube.com'
|
||||||
|
export const REDTUBE_BASE_URL_GAY: string = 'https://www.redtube.com/gay'
|
||||||
|
export const REDTUBE_BASE_URL_TRANS: string = 'https://www.redtube.com/redtube/transgender'
|
||||||
|
|
||||||
|
export const REDTUBE_BASE_SEARCH: string = 'https://www.redtube.com/?search='
|
||||||
|
export const REDTUBE_BASE_GAY_SEARCH: string = 'https://www.redtube.com/gay?search='
|
||||||
|
|
||||||
|
// XHAMSTER
|
||||||
|
export const XHAMSTER_BASE_URL = 'https://xhamster.com'
|
||||||
|
export const XHAMSTER_BASE_URL_VIDEOS = 'https://xhamster.com/videos'
|
||||||
|
|
||||||
|
export const XHAMSTER_BASE_URL_ETERO = 'https://xhamster.com/newest'
|
||||||
|
export const XHAMSTER_BASE_URL_GAY = 'https://xhamster.com/gay/newest'
|
||||||
|
export const XHAMSTER_BASE_URL_TRANS = 'https://xhamster.com/shemale/newest'
|
||||||
|
|
||||||
|
export const XHAMSTER_BASE_SEARCH = 'https://xhamster.com/search/'
|
||||||
|
export const XHAMSTER_BASE_SEARCH_GAY = 'https://xhamster.com/gay/search/'
|
||||||
|
export const XHAMSTER_BASE_SEARCH_TRANS = 'https://xhamster.com/shemale/search/'
|
|
@ -12,10 +12,20 @@ export interface GalleryData {
|
||||||
platform: Platforms
|
platform: Platforms
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VideoSourceItem {
|
||||||
|
type: string,
|
||||||
|
src: string,
|
||||||
|
size: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface VideoData {
|
export interface VideoData {
|
||||||
lowResUrl: string,
|
|
||||||
hiResUrl?: string,
|
|
||||||
hlsUrl?: string
|
hlsUrl?: string
|
||||||
|
srcSet?: VideoSourceItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MindGeekVideoSrcElem {
|
||||||
|
videoUrl: string
|
||||||
|
quality: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoAgent {
|
export interface VideoAgent {
|
||||||
|
|
|
@ -6,7 +6,11 @@ export enum Cookies {
|
||||||
|
|
||||||
export enum Platforms {
|
export enum Platforms {
|
||||||
xvideos= 'xvideos',
|
xvideos= 'xvideos',
|
||||||
xnxx= 'xnxx'
|
xnxx= 'xnxx',
|
||||||
|
pornhub= 'pornhub',
|
||||||
|
youporn= 'youporn',
|
||||||
|
redtube= 'redtube',
|
||||||
|
xhamster= 'xhamster'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum XVideosCatQueryMap {
|
export enum XVideosCatQueryMap {
|
||||||
|
@ -21,6 +25,27 @@ export enum XVideosOrientations {
|
||||||
trans= 'trans'
|
trans= 'trans'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PornHubOrientations {
|
||||||
|
generic= 'generic',
|
||||||
|
gay= 'gay'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum YouPornOrientations {
|
||||||
|
generic= 'generic'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RedTubeOrientations {
|
||||||
|
etero= 'etero',
|
||||||
|
gay= 'gay',
|
||||||
|
trans= 'trans'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum XHamsterOrientations {
|
||||||
|
etero= 'etero',
|
||||||
|
gay= 'gay',
|
||||||
|
trans= 'trans'
|
||||||
|
}
|
||||||
|
|
||||||
export enum Themes {
|
export enum Themes {
|
||||||
light= 'light',
|
light= 'light',
|
||||||
dark= 'dark',
|
dark= 'dark',
|
||||||
|
@ -33,3 +58,12 @@ export interface LangOption {
|
||||||
label: string;
|
label: string;
|
||||||
code: string;
|
code: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const OrientationMapper = {
|
||||||
|
[Platforms.xvideos]: XVideosOrientations,
|
||||||
|
[Platforms.xnxx]: XVideosOrientations,
|
||||||
|
[Platforms.pornhub]: PornHubOrientations,
|
||||||
|
[Platforms.youporn]: YouPornOrientations,
|
||||||
|
[Platforms.redtube]: RedTubeOrientations,
|
||||||
|
[Platforms.xhamster]: XHamsterOrientations
|
||||||
|
}
|
|
@ -4,10 +4,18 @@ import { Platforms } from "@/meta/settings";
|
||||||
|
|
||||||
import { XVideosAgent } from "./scrape/xvideos/agent";
|
import { XVideosAgent } from "./scrape/xvideos/agent";
|
||||||
import { XNXXAgent } from "./scrape/xnxx/agent";
|
import { XNXXAgent } from "./scrape/xnxx/agent";
|
||||||
|
import { PornHubAgent } from "./scrape/pornhub/agent";
|
||||||
|
import { YouPornAgent } from "./scrape/youporn/agent";
|
||||||
|
import { RedTubeAgent } from "./scrape/redtube/agent";
|
||||||
|
import { XHamsterAgent } from "./scrape/xhamster/agent";
|
||||||
|
|
||||||
const AgentMapper = {
|
const AgentMapper = {
|
||||||
[Platforms.xvideos]: XVideosAgent,
|
[Platforms.xvideos]: XVideosAgent,
|
||||||
[Platforms.xnxx]: XNXXAgent
|
[Platforms.xnxx]: XNXXAgent,
|
||||||
|
[Platforms.pornhub]: PornHubAgent,
|
||||||
|
[Platforms.youporn]: YouPornAgent,
|
||||||
|
[Platforms.redtube]: RedTubeAgent,
|
||||||
|
[Platforms.xhamster]: XHamsterAgent
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VideoAgent {
|
export class VideoAgent {
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Platforms } from "@/meta/settings";
|
||||||
|
|
||||||
|
export const getEnabledPlatforms = ():string[] => {
|
||||||
|
if (process.env.DISABLED_PLATFORMS) {
|
||||||
|
const regex = /[ '\"]/g;
|
||||||
|
|
||||||
|
const disabledPlatforms: string[] = String(process.env.DISABLED_PLATFORMS).replace(regex, '').split(',')
|
||||||
|
|
||||||
|
return [...Object.values(Platforms)].filter(p => !disabledPlatforms.includes(p))
|
||||||
|
} else {
|
||||||
|
return [...Object.values(Platforms)]
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ const getRandomUserAgent = (): string => {
|
||||||
return userAgents[rand]
|
return userAgents[rand]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getHeaders = (host:string = XVIDEOS_BASE_URL) => {
|
export const getHeaders = (host:string) => {
|
||||||
return {
|
return {
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": getRandomUserAgent(),
|
"User-Agent": getRandomUserAgent(),
|
||||||
|
@ -32,7 +32,23 @@ export const getHeaders = (host:string = XVIDEOS_BASE_URL) => {
|
||||||
"Sec-Fetch-Dest": "document",
|
"Sec-Fetch-Dest": "document",
|
||||||
"Sec-Fetch-Mode": "navigate",
|
"Sec-Fetch-Mode": "navigate",
|
||||||
"Sec-Fetch-Site": "none",
|
"Sec-Fetch-Site": "none",
|
||||||
"Host": removeHttpS(host)
|
"Host": removeHttpS(host),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHeadersWithCookie = (host:string, cookie: string) => {
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": getRandomUserAgent(),
|
||||||
|
"Accept-Language": "en-gb, en, en-US, it",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Sec-Fetch-Dest": "document",
|
||||||
|
"Sec-Fetch-Mode": "navigate",
|
||||||
|
"Sec-Fetch-Site": "none",
|
||||||
|
"Host": removeHttpS(host),
|
||||||
|
"Cookie": cookie
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
|
@ -0,0 +1,55 @@
|
||||||
|
export const findGetMediaUrlInTagblock = (
|
||||||
|
tagBlock: string, key?: string): string | null => {
|
||||||
|
|
||||||
|
const getMediaIndex = tagBlock.indexOf(key ?? 'get_media');
|
||||||
|
|
||||||
|
if (getMediaIndex === -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = tagBlock.lastIndexOf('"', getMediaIndex);
|
||||||
|
const end = tagBlock.indexOf('"', getMediaIndex);
|
||||||
|
|
||||||
|
const substr = tagBlock.substring(start, end);
|
||||||
|
|
||||||
|
if (substr.length > 0) {
|
||||||
|
return substr.replace(/\\/g, '').replace(/"/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findGetRelatedUrlInTagblock = (
|
||||||
|
tagBlock: string): string | null => {
|
||||||
|
|
||||||
|
const getMediaIndex = tagBlock.indexOf('player_related_datas');
|
||||||
|
|
||||||
|
if (getMediaIndex === -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = tagBlock.lastIndexOf('"', getMediaIndex);
|
||||||
|
const end = tagBlock.indexOf('"', getMediaIndex);
|
||||||
|
|
||||||
|
const substr = tagBlock.substring(start, end);
|
||||||
|
|
||||||
|
if (substr.length > 0) {
|
||||||
|
return substr.replace(/\\/g, '').replace(/"/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSessionCookie = (responseSetCookies: string[]): string => {
|
||||||
|
let pieces: string[] = []
|
||||||
|
|
||||||
|
responseSetCookies.map((elem, key) => {
|
||||||
|
if (elem.includes('platform=') || elem.includes('ss=') || elem.includes('fg_')) {
|
||||||
|
pieces.push(elem.split(';')[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const sessionCookie = pieces.join('; ');
|
||||||
|
|
||||||
|
return sessionCookie
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { FetchParams, GalleryData, VideoAgent, VideoData } from "@/meta/data";
|
||||||
|
import { fetchPornHubGalleryData } from "./gallery";
|
||||||
|
import { fetchPornHubVideoData } from "./video";
|
||||||
|
|
||||||
|
export class PornHubAgent implements VideoAgent {
|
||||||
|
|
||||||
|
public getGallery = async (params?: FetchParams): Promise<GalleryData[]> => {
|
||||||
|
return await fetchPornHubGalleryData(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getVideo = async (id: string, params?: FetchParams): Promise<[VideoData, GalleryData[]]> => {
|
||||||
|
return await fetchPornHubVideoData(id, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { PORNHUB_BASE_URL } from "@/constants/urls";
|
||||||
|
import { FetchParams, GalleryData } from "@/meta/data";
|
||||||
|
import { getHeaders } from "../common/headers";
|
||||||
|
import { getDataFromRedis, storeDataIntoRedis } from "@/redis/client";
|
||||||
|
import { getPornHubQueryUrl, getPornHubResultsWrapperId } from "./url";
|
||||||
|
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
|
||||||
|
import axios, { AxiosError } from "axios";
|
||||||
|
import { Platforms } from "@/meta/settings";
|
||||||
|
|
||||||
|
import { DEFAULT_PORNHUB_GALLERY_EXPIRY } from "@/constants/redis";
|
||||||
|
|
||||||
|
export const fetchPornHubGalleryData = async (params?: FetchParams): Promise<GalleryData[]> => {
|
||||||
|
|
||||||
|
let data: GalleryData[] = [];
|
||||||
|
|
||||||
|
const reqHeaders = getHeaders(PORNHUB_BASE_URL)
|
||||||
|
|
||||||
|
const queryUrl = await getPornHubQueryUrl(params?.query)
|
||||||
|
|
||||||
|
const cachedData = await getDataFromRedis(queryUrl)
|
||||||
|
|
||||||
|
if (cachedData) {
|
||||||
|
return cachedData as GalleryData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.get(queryUrl, reqHeaders)
|
||||||
|
|
||||||
|
.then(async response => {
|
||||||
|
|
||||||
|
const html = response.data;
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
const wrapperId = await getPornHubResultsWrapperId(params?.query)
|
||||||
|
|
||||||
|
const thumbs = $(wrapperId);
|
||||||
|
|
||||||
|
thumbs.map((key, thumb) => {
|
||||||
|
|
||||||
|
const videoUrl = $(thumb).find(".pcVideoListItem a").attr("href")?.split('=')[1];
|
||||||
|
const imgUrl = $(thumb).find(".pcVideoListItem a img").attr("src")
|
||||||
|
const text = $(thumb).find(".pcVideoListItem a").attr("title")
|
||||||
|
|
||||||
|
videoUrl && imgUrl && text && data.push({
|
||||||
|
videoUrl,
|
||||||
|
imgUrl,
|
||||||
|
text,
|
||||||
|
platform: Platforms.pornhub
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.length > 0) {
|
||||||
|
await storeDataIntoRedis(queryUrl, data, DEFAULT_PORNHUB_GALLERY_EXPIRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
}).catch((error: AxiosError) => {
|
||||||
|
// handle errors
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { PORNHUB_BASE_URL, PORNHUB_BASE_URL_GAY, PORNHUB_BASE_URL_GAY_SEARCH } from "@/constants/urls"
|
||||||
|
import axios, { AxiosHeaders } from "axios"
|
||||||
|
import { getHeadersWithCookie } from "../common/headers"
|
||||||
|
import { GalleryData, MindGeekVideoSrcElem, VideoSourceItem } from "@/meta/data"
|
||||||
|
import { Cookies, Platforms, PornHubOrientations } from "@/meta/settings"
|
||||||
|
import { getCookie } from "@/utils/cookies/read"
|
||||||
|
import { encodeUrl } from "@/utils/string"
|
||||||
|
import { DEFAULT_VIDEO_STREAM_ROUTE_PREFIX } from "@/constants/stream"
|
||||||
|
|
||||||
|
export const getPornHubQueryUrl = async (query?: string): Promise<string> => {
|
||||||
|
const orientation = await getCookie(Cookies.orientation)
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
return `${orientation && orientation.value == PornHubOrientations.gay ?
|
||||||
|
PORNHUB_BASE_URL_GAY_SEARCH :
|
||||||
|
PORNHUB_BASE_URL}/video/search?search=${query}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return orientation && orientation.value == PornHubOrientations.gay ? PORNHUB_BASE_URL_GAY : PORNHUB_BASE_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPornHubResultsWrapperId = async (query?: string): Promise<string> => {
|
||||||
|
const orientation = await getCookie(Cookies.orientation)
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
return "#videoSearchResult li"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation && orientation.value == PornHubOrientations.gay) {
|
||||||
|
return "#videoCategory li"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "#singleFeedSection li"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPornHubMediaUrlList = async (url: string, sessionCookie: string): Promise<VideoSourceItem[]> => {
|
||||||
|
|
||||||
|
const headersWithCookie = getHeadersWithCookie(PORNHUB_BASE_URL, sessionCookie)
|
||||||
|
|
||||||
|
let videos: VideoSourceItem[] = []
|
||||||
|
|
||||||
|
await axios.get(url, headersWithCookie)
|
||||||
|
.then(async response => {
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
|
||||||
|
videos = await response.data.map((elem: MindGeekVideoSrcElem) => ({
|
||||||
|
src: `${DEFAULT_VIDEO_STREAM_ROUTE_PREFIX}/${Platforms.pornhub}/${encodeUrl(elem?.videoUrl)}`,
|
||||||
|
type: 'video/mp4',
|
||||||
|
size: elem?.quality
|
||||||
|
})) as VideoSourceItem[]
|
||||||
|
|
||||||
|
return videos
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(error => console.log(error))
|
||||||
|
|
||||||
|
return videos
|
||||||
|
}
|
||||||
|
|
||||||
|
function containsAtLeastThreeSpaces(input: string): boolean {
|
||||||
|
// Conta il numero di spazi nella stringa
|
||||||
|
const spaceCount = (input.match(/ /g) || []).length;
|
||||||
|
// Verifica se ci sono almeno tre spazi
|
||||||
|
return spaceCount >= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPornHubRelatedVideoData = async (url: string, sessionCookie: string): Promise<GalleryData[]> => {
|
||||||
|
|
||||||
|
const headersWithCookie = getHeadersWithCookie(PORNHUB_BASE_URL, sessionCookie)
|
||||||
|
|
||||||
|
let gallery: GalleryData[] = []
|
||||||
|
|
||||||
|
await axios.get(url, headersWithCookie)
|
||||||
|
.then(async response => {
|
||||||
|
|
||||||
|
if (response.data?.related) {
|
||||||
|
|
||||||
|
Array(response.data.related).map((related: any[], key) => {
|
||||||
|
|
||||||
|
related.map((rel: string[], key) => {
|
||||||
|
|
||||||
|
let galleryElem: GalleryData = {
|
||||||
|
videoUrl: '',
|
||||||
|
imgUrl: '',
|
||||||
|
text: '',
|
||||||
|
platform: Platforms.pornhub
|
||||||
|
}
|
||||||
|
|
||||||
|
rel.map((str, key) => {
|
||||||
|
|
||||||
|
if (String(str).includes('.jpg')) {
|
||||||
|
galleryElem.imgUrl = str;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(str).includes('viewkey')) {
|
||||||
|
galleryElem.videoUrl = str.split('=')[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containsAtLeastThreeSpaces(String(str))) {
|
||||||
|
galleryElem.text = str;
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
gallery.push(galleryElem)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
return gallery;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(error => console.log(error))
|
||||||
|
|
||||||
|
return gallery
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { PORNHUB_BASE_URL, PORNHUB_BASE_URL_VIDEO } from "@/constants/urls";
|
||||||
|
import { FetchParams, GalleryData, VideoData, VideoSourceItem } from "@/meta/data";
|
||||||
|
import { getHeaders } from "../common/headers";
|
||||||
|
import { getDataFromRedis, storeDataIntoRedis } from "@/redis/client";
|
||||||
|
import { DEFAULT_PORNHUB_GALLERY_EXPIRY, DEFAULT_PORNHUB_VIDEO_EXPIRY, DEFAULT_RELATED_VIDEO_KEY_PATH } from "@/constants/redis";
|
||||||
|
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
|
||||||
|
import axios, { AxiosError } from "axios";
|
||||||
|
import { createSessionCookie, findGetMediaUrlInTagblock, findGetRelatedUrlInTagblock } from "../common/mindgeek";
|
||||||
|
import { getPornHubMediaUrlList, getPornHubRelatedVideoData } from "./url";
|
||||||
|
|
||||||
|
export const fetchPornHubVideoData = async (videoId: string, params?: FetchParams): Promise<[VideoData, GalleryData[]]> => {
|
||||||
|
|
||||||
|
let data: VideoData = {
|
||||||
|
hlsUrl: '',
|
||||||
|
srcSet: []
|
||||||
|
}
|
||||||
|
|
||||||
|
let relatedData: GalleryData[] = [];
|
||||||
|
|
||||||
|
let mediaUrl, relatedUrl, sessionCookie, convertedData: VideoSourceItem[]
|
||||||
|
|
||||||
|
let reqHeaders = getHeaders(PORNHUB_BASE_URL)
|
||||||
|
|
||||||
|
const queryUrl = `${PORNHUB_BASE_URL_VIDEO}${videoId.replace(/\//g, '')}`
|
||||||
|
|
||||||
|
const cachedVideoData = await getDataFromRedis(queryUrl)
|
||||||
|
const cachedRelatedData = await getDataFromRedis(queryUrl + DEFAULT_RELATED_VIDEO_KEY_PATH)
|
||||||
|
|
||||||
|
if (cachedVideoData) {
|
||||||
|
return [cachedVideoData as VideoData, cachedRelatedData as GalleryData[] ?? []]
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.get(queryUrl, reqHeaders)
|
||||||
|
|
||||||
|
.then(async response => {
|
||||||
|
|
||||||
|
sessionCookie = response?.headers["set-cookie"] ? createSessionCookie(response?.headers["set-cookie"]) : '';
|
||||||
|
|
||||||
|
const html = response.data;
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
const scriptTags = $("script");
|
||||||
|
|
||||||
|
scriptTags.map((idx, elem) => {
|
||||||
|
const getMediaUrl = findGetMediaUrlInTagblock($(elem).toString()) ?? null
|
||||||
|
|
||||||
|
if (getMediaUrl) {
|
||||||
|
mediaUrl = getMediaUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
scriptTags.map((idx, elem) => {
|
||||||
|
const getRelatedUrl = findGetRelatedUrlInTagblock($(elem).toString()) ?? null
|
||||||
|
|
||||||
|
if (getRelatedUrl) {
|
||||||
|
relatedUrl = getRelatedUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}).catch((error: AxiosError) => {
|
||||||
|
// error handling goes here
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sessionCookie && mediaUrl) {
|
||||||
|
convertedData = await getPornHubMediaUrlList(mediaUrl, sessionCookie)
|
||||||
|
data.srcSet = convertedData.reverse()
|
||||||
|
|
||||||
|
await storeDataIntoRedis(queryUrl, data, DEFAULT_PORNHUB_VIDEO_EXPIRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionCookie && relatedUrl) {
|
||||||
|
if (cachedRelatedData) {
|
||||||
|
relatedData = cachedRelatedData as GalleryData[]
|
||||||
|
} else {
|
||||||
|
relatedData = await getPornHubRelatedVideoData(relatedUrl, sessionCookie)
|
||||||
|
|
||||||
|
await storeDataIntoRedis(queryUrl + DEFAULT_RELATED_VIDEO_KEY_PATH, relatedData, DEFAULT_PORNHUB_GALLERY_EXPIRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ data, relatedData ]
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { FetchParams, GalleryData, VideoAgent, VideoData } from "@/meta/data";
|
||||||
|
import { fetchRedTubeGalleryData } from "./gallery";
|
||||||
|
import { fetchRedTubeVideoData } from "./video";
|
||||||
|
|
||||||
|
export class RedTubeAgent implements VideoAgent {
|
||||||
|
|
||||||
|
public getGallery = async (params?: FetchParams): Promise<GalleryData[]> => {
|
||||||
|
return await fetchRedTubeGalleryData(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getVideo = async (id: string, params?: FetchParams): Promise<[VideoData, GalleryData[]]> => {
|
||||||
|
return await fetchRedTubeVideoData(id, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { FetchParams, GalleryData } from "@/meta/data";
|
||||||
|
import { getHeaders } from "../common/headers";
|
||||||
|
import { REDTUBE_BASE_URL } from "@/constants/urls";
|
||||||
|
import { getRedTubeQueryUrl, getRedTubeResultsWrapperId } from "./url";
|
||||||
|
import { getDataFromRedis, storeDataIntoRedis } from "@/redis/client";
|
||||||
|
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
|
||||||
|
import axios, { AxiosError } from "axios";
|
||||||
|
import { Platforms } from "@/meta/settings";
|
||||||
|
import { DEFAULT_REDTUBE_GALLERY_EXPIRY } from "@/constants/redis";
|
||||||
|
|
||||||
|
export const fetchRedTubeGalleryData = async (params?: FetchParams): Promise<GalleryData[]> => {
|
||||||
|
|
||||||
|
let data: GalleryData[] = [];
|
||||||
|
|
||||||
|
const reqHeaders = getHeaders(REDTUBE_BASE_URL)
|
||||||
|
|
||||||
|
const queryUrl = await getRedTubeQueryUrl(params?.query)
|
||||||
|
|
||||||
|
const cachedData = await getDataFromRedis(queryUrl)
|
||||||
|
|
||||||
|
if (cachedData) {
|
||||||
|
return cachedData as GalleryData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.get(queryUrl, reqHeaders)
|
||||||
|
|
||||||
|
.then(async response => {
|
||||||
|
|
||||||
|
const html = response.data;
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
const wrapperId = await getRedTubeResultsWrapperId(params?.query)
|
||||||
|
|
||||||
|
const thumbs = $(wrapperId);
|
||||||
|
|
||||||
|
thumbs.map((key, thumb) => {
|
||||||
|
|
||||||
|
const videoUrl = $(thumb).find("a.video_link").attr("href")?.split('/')[1];
|
||||||
|
const imgUrl = $(thumb).find("img.js_thumbImageTag").attr("data-src")
|
||||||
|
const text = $(thumb).find("a.tm_video_title").attr("title");
|
||||||
|
|
||||||
|
videoUrl && imgUrl && text && data.push({
|
||||||
|
videoUrl,
|
||||||
|
imgUrl,
|
||||||
|
text,
|
||||||
|
platform: Platforms.redtube
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await storeDataIntoRedis(queryUrl, data, DEFAULT_REDTUBE_GALLERY_EXPIRY);
|
||||||
|
|
||||||
|
}).catch((error: AxiosError) => {
|
||||||
|
// handle errors
|
||||||
|
});
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { getCookie } from "@/utils/cookies/read"
|
||||||
|
|
||||||
|
import { Cookies, RedTubeOrientations } from "@/meta/settings"
|
||||||
|
import { REDTUBE_BASE_SEARCH, REDTUBE_BASE_GAY_SEARCH, REDTUBE_BASE_URL_GAY, REDTUBE_BASE_URL, REDTUBE_BASE_URL_TRANS } from "@/constants/urls"
|
||||||
|
import { getHeadersWithCookie } from "../common/headers"
|
||||||
|
import { MindGeekVideoSrcElem, VideoSourceItem } from "@/meta/data"
|
||||||
|
|
||||||
|
import axios from "axios"
|
||||||
|
|
||||||
|
export const getRedTubeQueryUrl = async (query?: string): Promise<string> => {
|
||||||
|
const orientation = await getCookie(Cookies.orientation)
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
return `${orientation && orientation.value == RedTubeOrientations.gay ?
|
||||||
|
REDTUBE_BASE_GAY_SEARCH :
|
||||||
|
REDTUBE_BASE_SEARCH}${query}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation && orientation.value == RedTubeOrientations.gay ) {
|
||||||
|
return REDTUBE_BASE_URL_GAY
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation && orientation.value == RedTubeOrientations.trans ) {
|
||||||
|
return REDTUBE_BASE_URL_TRANS
|
||||||
|
}
|
||||||
|
|
||||||
|
return REDTUBE_BASE_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRedTubeResultsWrapperId = async (query?: string): Promise<string> => {
|
||||||
|
const orientation = await getCookie(Cookies.orientation)
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
return ".videos_grid li"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation && ((orientation.value == RedTubeOrientations.gay) || (orientation.value == RedTubeOrientations.trans))) {
|
||||||
|
return "#block_browse li"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "#most_recent_videos li"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRedTubeMediaUrlList = async (url: string, sessionCookie: string): Promise<VideoSourceItem[]> => {
|
||||||
|
|
||||||
|
const headersWithCookie = getHeadersWithCookie(REDTUBE_BASE_URL, sessionCookie)
|
||||||
|
|
||||||
|
let videos: VideoSourceItem[] = []
|
||||||
|
|
||||||
|
await axios.get(url, headersWithCookie)
|
||||||
|
.then(async response => {
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
|
||||||
|
videos = await response.data.map((elem: MindGeekVideoSrcElem) => ({
|
||||||
|
src: elem?.videoUrl,
|
||||||
|
type: 'video/mp4',
|
||||||
|
size: elem?.quality
|
||||||
|
})) as VideoSourceItem[]
|
||||||
|
|
||||||
|
return videos
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(error => console.log(error))
|
||||||
|
|
||||||
|
return videos
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { REDTUBE_BASE_URL } from "@/constants/urls";
|
||||||
|
import { FetchParams, GalleryData, VideoData, VideoSourceItem } from "@/meta/data";
|
||||||
|
import { getHeaders } from "../common/headers";
|
||||||
|
import { getDataFromRedis, storeDataIntoRedis } from "@/redis/client";
|
||||||
|
import { DEFAULT_REDTUBE_GALLERY_EXPIRY, DEFAULT_REDTUBE_VIDEO_EXPIRY, DEFAULT_RELATED_VIDEO_KEY_PATH } from "@/constants/redis";
|
||||||
|
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
|
||||||
|
import axios, { AxiosError } from "axios";
|
||||||
|
import { createSessionCookie, findGetMediaUrlInTagblock } from "../common/mindgeek";
|
||||||
|
import { Platforms } from "@/meta/settings";
|
||||||
|
import { getRedTubeMediaUrlList } from "./url";
|
||||||
|
|
||||||
|
export const fetchRedTubeVideoData = async (videoId: string, params?: FetchParams): Promise<[VideoData, GalleryData[]]> => {
|
||||||
|
|
||||||
|
let data: VideoData = {
|
||||||
|
hlsUrl: '',
|
||||||
|
srcSet: []
|
||||||
|
}
|
||||||
|
|
||||||
|
let relatedData: GalleryData[] = [];
|
||||||
|
|
||||||
|
let mediaUrl, sessionCookie, convertedData: VideoSourceItem[]
|
||||||
|
|
||||||
|
let reqHeaders = getHeaders(REDTUBE_BASE_URL)
|
||||||
|
|
||||||
|
const queryUrl = `${REDTUBE_BASE_URL}/${videoId.replace(/\//g, '')}`
|
||||||
|
|
||||||
|
const cachedVideoData = await getDataFromRedis(queryUrl)
|
||||||
|
const cachedRelatedData = await getDataFromRedis(queryUrl + DEFAULT_RELATED_VIDEO_KEY_PATH)
|
||||||
|
|
||||||
|
if (cachedVideoData) {
|
||||||
|
return [cachedVideoData as VideoData, cachedRelatedData as GalleryData[] ?? []]
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.get(queryUrl, reqHeaders)
|
||||||
|
|
||||||
|
.then(async response => {
|
||||||
|
|
||||||
|
sessionCookie = response?.headers["set-cookie"] ? createSessionCookie(response?.headers["set-cookie"]) : '';
|
||||||
|
|
||||||
|
const html = response.data;
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
const scriptTags = $("script");
|
||||||
|
|
||||||
|
scriptTags.map((idx, elem) => {
|
||||||
|
const getMediaUrl = findGetMediaUrlInTagblock($(elem).toString().replace(/\\/g, ''), 'media/mp4') ?? null
|
||||||
|
|
||||||
|
if (getMediaUrl) {
|
||||||
|
mediaUrl = `${REDTUBE_BASE_URL}${getMediaUrl}`
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapperId = "#related_videos_center li.tm_video_block"
|
||||||
|
|
||||||
|
const thumbs = $(wrapperId);
|
||||||
|
|
||||||
|
thumbs.map((key, thumb) => {
|
||||||
|
|
||||||
|
const videoUrl = $(thumb).find("a.tm_video_link").attr("href")?.split('/')[1];
|
||||||
|
const imgUrl = $(thumb).find("img.js_thumbImageTag").attr("data-src")
|
||||||
|
const text = $(thumb).find("a.tm_video_title").attr("title");
|
||||||
|
|
||||||
|
videoUrl && imgUrl && text && relatedData.push({
|
||||||
|
videoUrl,
|
||||||
|
imgUrl,
|
||||||
|
text,
|
||||||
|
platform: Platforms.redtube
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}).catch((error: AxiosError) => {
|
||||||
|
// error handling goes here
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sessionCookie && mediaUrl) {
|
||||||
|
convertedData = await getRedTubeMediaUrlList(mediaUrl, sessionCookie)
|
||||||
|
data.srcSet = convertedData.reverse()
|
||||||
|
|
||||||
|
await storeDataIntoRedis(queryUrl, data, DEFAULT_REDTUBE_VIDEO_EXPIRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedData.length > 0) {
|
||||||
|
await storeDataIntoRedis(queryUrl + DEFAULT_RELATED_VIDEO_KEY_PATH, relatedData, DEFAULT_REDTUBE_GALLERY_EXPIRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ data, relatedData ]
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { FetchParams, GalleryData, VideoAgent, VideoData } from "@/meta/data";
|
||||||
|
import { fetchXHamsterGalleryData } from "./gallery";
|
||||||
|
import { fetchXHamsterVideoData } from "./video";
|
||||||
|
|
||||||
|
export class XHamsterAgent implements VideoAgent {
|
||||||
|
|
||||||
|
public getGallery = async (params?: FetchParams): Promise<GalleryData[]> => {
|
||||||
|
return await fetchXHamsterGalleryData(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getVideo = async (id: string, params?: FetchParams): Promise<[VideoData, GalleryData[]]> => {
|
||||||
|
return await fetchXHamsterVideoData(id, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { XHAMSTER_BASE_URL, XHAMSTER_BASE_URL_VIDEOS } from "@/constants/urls";
|
||||||
|
import { FetchParams, GalleryData } from "@/meta/data";
|
||||||
|
import { getHeaders } from "../common/headers";
|
||||||
|
import { getXHamsterQueryUrl } from "./url";
|
||||||
|
import { getDataFromRedis, storeDataIntoRedis } from "@/redis/client";
|
||||||
|
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
|
||||||
|
import axios, { AxiosError } from "axios";
|
||||||
|
import { DEFAULT_XHAMSTER_GALLERY_EXPIRY } from "@/constants/redis";
|
||||||
|
import { Platforms } from "@/meta/settings";
|
||||||
|
|
||||||
|
export const fetchXHamsterGalleryData = async (params?: FetchParams): Promise<GalleryData[]> => {
|
||||||
|
|
||||||
|
let data: GalleryData[] = [];
|
||||||
|
|
||||||
|
const reqHeaders = getHeaders(XHAMSTER_BASE_URL)
|
||||||
|
|
||||||
|
const queryUrl = await getXHamsterQueryUrl(params?.query)
|
||||||
|
|
||||||
|
const cachedData = await getDataFromRedis(queryUrl)
|
||||||
|
|
||||||
|
if (cachedData) {
|
||||||
|
return cachedData as GalleryData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.get(queryUrl, reqHeaders)
|
||||||
|
|
||||||
|
.then(async response => {
|
||||||
|
|
||||||
|
const html = response.data;
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
const wrapperId = '.thumb-list .thumb-list__item'
|
||||||
|
|
||||||
|
const thumbs = $(wrapperId);
|
||||||
|
|
||||||
|
thumbs.map((key, thumb) => {
|
||||||
|
|
||||||
|
const videoUrl = $(thumb).find("a.video-thumb__image-container").attr("href")?.replace(XHAMSTER_BASE_URL_VIDEOS, '')
|
||||||
|
const imgUrl = $(thumb).find("a.video-thumb__image-container img").attr("src")
|
||||||
|
const text = $(thumb).find("a.video-thumb-info__name").attr("title")
|
||||||
|
|
||||||
|
videoUrl && imgUrl && text && data.push({
|
||||||
|
videoUrl,
|
||||||
|
imgUrl,
|
||||||
|
text,
|
||||||
|
platform: Platforms.xhamster
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.length > 0) {
|
||||||
|
await storeDataIntoRedis(queryUrl, data, DEFAULT_XHAMSTER_GALLERY_EXPIRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
}).catch((error: AxiosError) => {
|
||||||
|
// handle errors
|
||||||
|
});
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { XHAMSTER_BASE_SEARCH, XHAMSTER_BASE_SEARCH_GAY, XHAMSTER_BASE_SEARCH_TRANS, XHAMSTER_BASE_URL_ETERO, XHAMSTER_BASE_URL_GAY, XHAMSTER_BASE_URL_TRANS } from "@/constants/urls"
|
||||||
|
import { Cookies, XHamsterOrientations } from "@/meta/settings"
|
||||||
|
import { getCookie } from "@/utils/cookies/read"
|
||||||
|
|
||||||
|
export const getXHamsterQueryUrl = async (query?: string): Promise<string> => {
|
||||||
|
const orientation = await getCookie(Cookies.orientation)
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
if (orientation && orientation.value == XHamsterOrientations.gay) {
|
||||||
|
return XHAMSTER_BASE_SEARCH_GAY + query + '?revert=orientation'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation && orientation.value == XHamsterOrientations.trans) {
|
||||||
|
return XHAMSTER_BASE_SEARCH_TRANS + query + '?revert=orientation'
|
||||||
|
}
|
||||||
|
|
||||||
|
return XHAMSTER_BASE_SEARCH + query
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (orientation && orientation.value == XHamsterOrientations.gay) {
|
||||||
|
return XHAMSTER_BASE_URL_GAY
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation && orientation.value == XHamsterOrientations.trans) {
|
||||||
|
return XHAMSTER_BASE_URL_TRANS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return XHAMSTER_BASE_URL_ETERO
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { XHAMSTER_BASE_URL, XHAMSTER_BASE_URL_VIDEOS } from "@/constants/urls";
|
||||||
|
import { FetchParams, GalleryData, VideoData, VideoSourceItem } from "@/meta/data";
|
||||||
|
import { getHeaders } from "../common/headers";
|
||||||
|
import { getDataFromRedis, storeDataIntoRedis } from "@/redis/client";
|
||||||
|
import { DEFAULT_RELATED_VIDEO_KEY_PATH, DEFAULT_XHAMSTER_GALLERY_EXPIRY, DEFAULT_XHAMSTER_VIDEO_EXPIRY } from "@/constants/redis";
|
||||||
|
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
|
||||||
|
import axios, { AxiosError } from "axios";
|
||||||
|
import { findGetMediaUrlInTagblock } from "../common/mindgeek";
|
||||||
|
import { Platforms } from "@/meta/settings";
|
||||||
|
import { encodeUrl } from "@/utils/string";
|
||||||
|
import { DEFAULT_VIDEO_STREAM_ROUTE_PREFIX } from "@/constants/stream";
|
||||||
|
|
||||||
|
export const fetchXHamsterVideoData = async (videoId: string, params?: FetchParams): Promise<[VideoData, GalleryData[]]> => {
|
||||||
|
|
||||||
|
let data: VideoData = {
|
||||||
|
srcSet: []
|
||||||
|
}
|
||||||
|
|
||||||
|
let relatedData: GalleryData[] = [];
|
||||||
|
|
||||||
|
let reqHeaders = getHeaders(XHAMSTER_BASE_URL);
|
||||||
|
|
||||||
|
const queryUrl = `${XHAMSTER_BASE_URL_VIDEOS}/${videoId.replace(/\//g, '')}`
|
||||||
|
|
||||||
|
const cachedVideoData = await getDataFromRedis(queryUrl)
|
||||||
|
const cachedRelatedData = await getDataFromRedis(queryUrl + DEFAULT_RELATED_VIDEO_KEY_PATH)
|
||||||
|
|
||||||
|
if (cachedVideoData) {
|
||||||
|
return [cachedVideoData as VideoData, cachedRelatedData as GalleryData[] ?? []]
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.get(queryUrl, reqHeaders)
|
||||||
|
|
||||||
|
.then(async response => {
|
||||||
|
|
||||||
|
const html = response.data;
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
const scriptTags = $("script");
|
||||||
|
|
||||||
|
scriptTags.map((idx, elem) => {
|
||||||
|
const hlsUrl = findGetMediaUrlInTagblock($(elem).toString().replace(/\\/g, ''), 'media=hls4') ?? null
|
||||||
|
|
||||||
|
if (hlsUrl) {
|
||||||
|
['144', '240', '360', '480', '720', '1080'].map((res: string) => {
|
||||||
|
let resUrl = findGetMediaUrlInTagblock($(elem).toString().replace(/\\/g, ''), `${res}p.h264.mp4`) ?? null
|
||||||
|
|
||||||
|
if (resUrl) {
|
||||||
|
data.srcSet?.push({
|
||||||
|
src: `${DEFAULT_VIDEO_STREAM_ROUTE_PREFIX}/${Platforms.xhamster}/${encodeUrl(resUrl)}`,
|
||||||
|
type: 'video/mp4',
|
||||||
|
size: res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapperId = '.thumb-list .thumb-list__item'
|
||||||
|
|
||||||
|
const thumbs = $(wrapperId);
|
||||||
|
|
||||||
|
thumbs.map((key, thumb) => {
|
||||||
|
|
||||||
|
const videoUrl = $(thumb).find("a.video-thumb__image-container").attr("href")?.replace(XHAMSTER_BASE_URL_VIDEOS, '')
|
||||||
|
const imgUrl = $(thumb).find("a.video-thumb__image-container img").attr("src")
|
||||||
|
const text = $(thumb).find("a.video-thumb-info__name").attr("title")
|
||||||
|
|
||||||
|
videoUrl && imgUrl && text && relatedData.push({
|
||||||
|
videoUrl,
|
||||||
|
imgUrl,
|
||||||
|
text,
|
||||||
|
platform: Platforms.xhamster
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}).catch((error: AxiosError) => {
|
||||||
|
// error handling goes here
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.srcSet && data.srcSet?.length > 0) {
|
||||||
|
await storeDataIntoRedis(queryUrl, data, DEFAULT_XHAMSTER_VIDEO_EXPIRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedData.length > 0) {
|
||||||
|
await storeDataIntoRedis(queryUrl + DEFAULT_RELATED_VIDEO_KEY_PATH, relatedData, DEFAULT_XHAMSTER_GALLERY_EXPIRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ data, relatedData ]
|
||||||
|
}
|
|
@ -14,7 +14,8 @@ import { getDataFromRedis, storeDataIntoRedis } from '@/redis/client';
|
||||||
export const fetchXNXXVideoData = async (videoId: string, params?: FetchParams): Promise<[VideoData, GalleryData[]]> => {
|
export const fetchXNXXVideoData = async (videoId: string, params?: FetchParams): Promise<[VideoData, GalleryData[]]> => {
|
||||||
|
|
||||||
let data: VideoData = {
|
let data: VideoData = {
|
||||||
lowResUrl: ''
|
hlsUrl: '',
|
||||||
|
srcSet: []
|
||||||
}
|
}
|
||||||
|
|
||||||
let related: GalleryData[] = [];
|
let related: GalleryData[] = [];
|
||||||
|
@ -50,11 +51,19 @@ export const fetchXNXXVideoData = async (videoId: string, params?: FetchParams):
|
||||||
const hlsUrl = findVideoUrlInsideTagStringByFunctionNameAndExtension($(elem).toString(), 'setVideoHLS', '.m3u8')
|
const hlsUrl = findVideoUrlInsideTagStringByFunctionNameAndExtension($(elem).toString(), 'setVideoHLS', '.m3u8')
|
||||||
|
|
||||||
if (lowResUrl) {
|
if (lowResUrl) {
|
||||||
data.lowResUrl = lowResUrl;
|
data.srcSet?.push({
|
||||||
|
src: lowResUrl,
|
||||||
|
type: 'video/mp4',
|
||||||
|
size: "480"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hiResUrl) {
|
if (hiResUrl) {
|
||||||
data.hiResUrl = hiResUrl
|
data.srcSet?.push({
|
||||||
|
src: hiResUrl,
|
||||||
|
type: 'video/mp4',
|
||||||
|
size: "720"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hlsUrl) {
|
if (hlsUrl) {
|
||||||
|
|
|
@ -8,12 +8,13 @@ import { Platforms } from '@/meta/settings';
|
||||||
|
|
||||||
import { getDataFromRedis, storeDataIntoRedis } from '@/redis/client';
|
import { getDataFromRedis, storeDataIntoRedis } from '@/redis/client';
|
||||||
import { DEFAULT_XVIDEOS_CONTENT_EXPIRY } from '@/constants/redis';
|
import { DEFAULT_XVIDEOS_CONTENT_EXPIRY } from '@/constants/redis';
|
||||||
|
import { XVIDEOS_BASE_URL } from '@/constants/urls';
|
||||||
|
|
||||||
export const fetchXVideosGalleryData = async (params?: FetchParams): Promise<GalleryData[]> => {
|
export const fetchXVideosGalleryData = async (params?: FetchParams): Promise<GalleryData[]> => {
|
||||||
|
|
||||||
let data: GalleryData[] = [];
|
let data: GalleryData[] = [];
|
||||||
|
|
||||||
const reqHeaders = getHeaders()
|
const reqHeaders = getHeaders(XVIDEOS_BASE_URL)
|
||||||
|
|
||||||
const queryUrl = await getXVideosQueryUrl(params?.query)
|
const queryUrl = await getXVideosQueryUrl(params?.query)
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,8 @@ import { DEFAULT_RELATED_VIDEO_KEY_PATH, DEFAULT_XVIDEOS_CONTENT_EXPIRY } from '
|
||||||
export const fetchXvideosVideoData = async (videoId: string, params?: FetchParams): Promise<[VideoData, GalleryData[]]> => {
|
export const fetchXvideosVideoData = async (videoId: string, params?: FetchParams): Promise<[VideoData, GalleryData[]]> => {
|
||||||
|
|
||||||
let data: VideoData = {
|
let data: VideoData = {
|
||||||
lowResUrl: ''
|
hlsUrl: '',
|
||||||
|
srcSet: []
|
||||||
}
|
}
|
||||||
|
|
||||||
let related: GalleryData[] = [];
|
let related: GalleryData[] = [];
|
||||||
|
@ -50,11 +51,19 @@ export const fetchXvideosVideoData = async (videoId: string, params?: FetchParam
|
||||||
const hlsUrl = findVideoUrlInsideTagStringByFunctionNameAndExtension($(elem).toString(), 'setVideoHLS', '.m3u8')
|
const hlsUrl = findVideoUrlInsideTagStringByFunctionNameAndExtension($(elem).toString(), 'setVideoHLS', '.m3u8')
|
||||||
|
|
||||||
if (lowResUrl) {
|
if (lowResUrl) {
|
||||||
data.lowResUrl = lowResUrl;
|
data.srcSet?.push({
|
||||||
|
src: lowResUrl,
|
||||||
|
type: 'video/mp4',
|
||||||
|
size: "480"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hiResUrl) {
|
if (hiResUrl) {
|
||||||
data.hiResUrl = hiResUrl
|
data.srcSet?.push({
|
||||||
|
src: hiResUrl,
|
||||||
|
type: 'video/mp4',
|
||||||
|
size: "720"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hlsUrl) {
|
if (hlsUrl) {
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { FetchParams, GalleryData, VideoAgent, VideoData } from "@/meta/data";
|
||||||
|
import { fetchYouPornGalleryData } from "./gallery";
|
||||||
|
import { fetchYouPornVideoData } from "./video";
|
||||||
|
|
||||||
|
export class YouPornAgent implements VideoAgent {
|
||||||
|
|
||||||
|
public getGallery = async (params?: FetchParams): Promise<GalleryData[]> => {
|
||||||
|
return await fetchYouPornGalleryData(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getVideo = async (id: string, params?: FetchParams): Promise<[VideoData, GalleryData[]]> => {
|
||||||
|
return await fetchYouPornVideoData(id, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { YOUPORN_BASE_URL } from "@/constants/urls";
|
||||||
|
import { FetchParams, GalleryData } from "@/meta/data";
|
||||||
|
import { getHeaders } from "../common/headers";
|
||||||
|
import { getYouPornQueryUrl } from "./url";
|
||||||
|
import { getDataFromRedis, storeDataIntoRedis } from "@/redis/client";
|
||||||
|
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
|
||||||
|
import axios, { AxiosError } from "axios";
|
||||||
|
import { Platforms } from "@/meta/settings";
|
||||||
|
import { DEFAULT_YOUPORN_GALLERY_EXPIRY } from "@/constants/redis";
|
||||||
|
|
||||||
|
export const fetchYouPornGalleryData = async (params?: FetchParams): Promise<GalleryData[]> => {
|
||||||
|
|
||||||
|
let data: GalleryData[] = [];
|
||||||
|
|
||||||
|
const reqHeaders = getHeaders(YOUPORN_BASE_URL)
|
||||||
|
|
||||||
|
const queryUrl = await getYouPornQueryUrl(params?.query)
|
||||||
|
|
||||||
|
const cachedData = await getDataFromRedis(queryUrl)
|
||||||
|
|
||||||
|
if (cachedData) {
|
||||||
|
return cachedData as GalleryData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.get(queryUrl, reqHeaders)
|
||||||
|
|
||||||
|
.then(async response => {
|
||||||
|
|
||||||
|
const html = response.data;
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
const wrapperId = params?.query ? ".searchResults .video-box" : ".tm_mostRecent_videos_section .video-box"
|
||||||
|
|
||||||
|
const thumbs = $(wrapperId);
|
||||||
|
|
||||||
|
thumbs.map((key, thumb) => {
|
||||||
|
|
||||||
|
const videoUrl = $(thumb).find("a.tm_video_link").attr("href")?.split('/')[2];
|
||||||
|
const imgUrl = $(thumb).find("img.thumb-image").attr("data-src")
|
||||||
|
const text = $(thumb).find("a.video-title").text();
|
||||||
|
|
||||||
|
videoUrl && imgUrl && text && data.push({
|
||||||
|
videoUrl,
|
||||||
|
imgUrl,
|
||||||
|
text,
|
||||||
|
platform: Platforms.youporn
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await storeDataIntoRedis(queryUrl, data, DEFAULT_YOUPORN_GALLERY_EXPIRY);
|
||||||
|
|
||||||
|
}).catch((error: AxiosError) => {
|
||||||
|
// handle errors
|
||||||
|
});
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { YOUPORN_BASE_SEARCH, YOUPORN_BASE_URL } from "@/constants/urls"
|
||||||
|
import { getHeadersWithCookie } from "../common/headers"
|
||||||
|
import axios from "axios"
|
||||||
|
import { MindGeekVideoSrcElem, VideoSourceItem } from "@/meta/data"
|
||||||
|
|
||||||
|
export const getYouPornQueryUrl = async (query?: string): Promise<string> => {
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
return `${YOUPORN_BASE_SEARCH}${query}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return YOUPORN_BASE_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getYouPornMediaUrlList = async (url: string, sessionCookie: string): Promise<VideoSourceItem[]> => {
|
||||||
|
|
||||||
|
const headersWithCookie = getHeadersWithCookie(YOUPORN_BASE_URL, sessionCookie)
|
||||||
|
|
||||||
|
let videos: VideoSourceItem[] = []
|
||||||
|
|
||||||
|
await axios.get(url, headersWithCookie)
|
||||||
|
.then(async response => {
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
|
||||||
|
videos = await response.data.map((elem: MindGeekVideoSrcElem) => ({
|
||||||
|
src: elem?.videoUrl,
|
||||||
|
type: 'video/mp4',
|
||||||
|
size: elem?.quality
|
||||||
|
})) as VideoSourceItem[]
|
||||||
|
|
||||||
|
return videos
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(error => console.log(error))
|
||||||
|
|
||||||
|
return videos
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { YOUPORN_BASE_URL, YOUPORN_BASE_URL_VIDEO } from "@/constants/urls";
|
||||||
|
import { FetchParams, GalleryData, VideoData, VideoSourceItem } from "@/meta/data";
|
||||||
|
import { getHeaders } from "../common/headers";
|
||||||
|
import { getDataFromRedis, storeDataIntoRedis } from "@/redis/client";
|
||||||
|
import { DEFAULT_RELATED_VIDEO_KEY_PATH, DEFAULT_YOUPORN_VIDEO_EXPIRY, DEFAULT_YOUPORN_GALLERY_EXPIRY } from "@/constants/redis";
|
||||||
|
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
|
||||||
|
import axios, { AxiosError } from "axios";
|
||||||
|
import { createSessionCookie, findGetMediaUrlInTagblock } from "../common/mindgeek";
|
||||||
|
import { getYouPornMediaUrlList } from "./url";
|
||||||
|
import { Platforms } from "@/meta/settings";
|
||||||
|
|
||||||
|
export const fetchYouPornVideoData = async (videoId: string, params?: FetchParams): Promise<[VideoData, GalleryData[]]> => {
|
||||||
|
|
||||||
|
let data: VideoData = {
|
||||||
|
hlsUrl: '',
|
||||||
|
srcSet: []
|
||||||
|
}
|
||||||
|
|
||||||
|
let relatedData: GalleryData[] = [];
|
||||||
|
|
||||||
|
let mediaUrl, sessionCookie, convertedData: VideoSourceItem[]
|
||||||
|
|
||||||
|
let reqHeaders = getHeaders(YOUPORN_BASE_URL)
|
||||||
|
|
||||||
|
const queryUrl = `${YOUPORN_BASE_URL_VIDEO}/${videoId.replace(/\//g, '')}`
|
||||||
|
|
||||||
|
const cachedVideoData = await getDataFromRedis(queryUrl)
|
||||||
|
const cachedRelatedData = await getDataFromRedis(queryUrl + DEFAULT_RELATED_VIDEO_KEY_PATH)
|
||||||
|
|
||||||
|
if (cachedVideoData) {
|
||||||
|
return [cachedVideoData as VideoData, cachedRelatedData as GalleryData[] ?? []]
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.get(queryUrl, reqHeaders)
|
||||||
|
|
||||||
|
.then(async response => {
|
||||||
|
|
||||||
|
sessionCookie = response?.headers["set-cookie"] ? createSessionCookie(response?.headers["set-cookie"]) : '';
|
||||||
|
|
||||||
|
const html = response.data;
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
const scriptTags = $("script");
|
||||||
|
|
||||||
|
scriptTags.map((idx, elem) => {
|
||||||
|
const getMediaUrl = findGetMediaUrlInTagblock($(elem).toString().replace(/\\/g, ''), 'media/mp4') ?? null
|
||||||
|
|
||||||
|
if (getMediaUrl) {
|
||||||
|
mediaUrl = getMediaUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapperId = "#relatedVideos .video-box"
|
||||||
|
|
||||||
|
const thumbs = $(wrapperId);
|
||||||
|
|
||||||
|
thumbs.map((key, thumb) => {
|
||||||
|
|
||||||
|
const videoUrl = $(thumb).find("a.tm_video_link").attr("href")?.split('/')[2];
|
||||||
|
const imgUrl = $(thumb).find("img.thumb-image").attr("data-src")
|
||||||
|
const text = $(thumb).find("a.video-title").text();
|
||||||
|
|
||||||
|
videoUrl && imgUrl && text && relatedData.push({
|
||||||
|
videoUrl,
|
||||||
|
imgUrl,
|
||||||
|
text,
|
||||||
|
platform: Platforms.youporn
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}).catch((error: AxiosError) => {
|
||||||
|
// error handling goes here
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sessionCookie && mediaUrl) {
|
||||||
|
convertedData = await getYouPornMediaUrlList(mediaUrl, sessionCookie)
|
||||||
|
data.srcSet = convertedData.reverse()
|
||||||
|
|
||||||
|
await storeDataIntoRedis(queryUrl, data, DEFAULT_YOUPORN_VIDEO_EXPIRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedData.length > 0) {
|
||||||
|
await storeDataIntoRedis(queryUrl + DEFAULT_RELATED_VIDEO_KEY_PATH, relatedData, DEFAULT_YOUPORN_GALLERY_EXPIRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ data, relatedData ]
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { DEFAULT_ENCODING_KEY } from "@/constants/encoding";
|
||||||
|
|
||||||
export const removeHttpS = (url: string): string => {
|
export const removeHttpS = (url: string): string => {
|
||||||
if (url.startsWith("http://")) {
|
if (url.startsWith("http://")) {
|
||||||
return url.slice(7);
|
return url.slice(7);
|
||||||
|
@ -13,4 +15,59 @@ export const encodeVideoUrlPath = (input: string): string => {
|
||||||
|
|
||||||
export const decodeVideoUrlPath = (input: string): string => {
|
export const decodeVideoUrlPath = (input: string): string => {
|
||||||
return `/${decodeURIComponent(input)}`;
|
return `/${decodeURIComponent(input)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getEncodingKey = ():string => {
|
||||||
|
return process.env.ENCODING_KEY ?? DEFAULT_ENCODING_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funzione per codifica Base64 URL-safe
|
||||||
|
const base64UrlEncode = (input: string): string => {
|
||||||
|
return btoa(input)
|
||||||
|
.replace(/\+/g, '-')
|
||||||
|
.replace(/\//g, '_')
|
||||||
|
.replace(/=+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funzione per decodifica Base64 URL-safe
|
||||||
|
const base64UrlDecode = (input: string): string => {
|
||||||
|
let base64 = input
|
||||||
|
.replace(/-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
|
||||||
|
// Aggiungi padding se necessario
|
||||||
|
while (base64.length % 4) {
|
||||||
|
base64 += '=';
|
||||||
|
}
|
||||||
|
|
||||||
|
return atob(base64);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeUrl(url: string): string {
|
||||||
|
const key = getEncodingKey();
|
||||||
|
|
||||||
|
// Convert the URL and key to UTF-8 bytes
|
||||||
|
const urlBytes = new TextEncoder().encode(url);
|
||||||
|
const keyBytes = new TextEncoder().encode(key);
|
||||||
|
|
||||||
|
// XOR the bytes of the URL with the key bytes
|
||||||
|
const encodedBytes = urlBytes.map((byte, index) => byte ^ keyBytes[index % keyBytes.length]);
|
||||||
|
|
||||||
|
// Convert the XORed bytes to a base64 URL-safe string
|
||||||
|
//@ts-ignore
|
||||||
|
return base64UrlEncode(String.fromCharCode(...encodedBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeUrl(encodedUrl: string): string {
|
||||||
|
const key = getEncodingKey();
|
||||||
|
|
||||||
|
// Decode the base64 URL-safe string to get the XORed bytes
|
||||||
|
const encodedBytes = Uint8Array.from(base64UrlDecode(encodedUrl), char => char.charCodeAt(0));
|
||||||
|
const keyBytes = new TextEncoder().encode(key);
|
||||||
|
|
||||||
|
// XOR the encoded bytes with the key bytes to get the original URL bytes
|
||||||
|
const urlBytes = encodedBytes.map((byte, index) => byte ^ keyBytes[index % keyBytes.length]);
|
||||||
|
|
||||||
|
// Convert the bytes back to a string
|
||||||
|
return new TextDecoder().decode(urlBytes);
|
||||||
|
}
|
Loading…
Reference in New Issue