Compare commits

...

83 Commits
v0.2.0 ... main

Author SHA1 Message Date
lamacchinadesiderante e981b174e3 Merge pull request 'Release v0.4.1' (#118) from develop into main
Reviewed-on: #118
2024-06-09 17:45:37 +00:00
lamacchinadesiderante 1a8c70cbee Merge pull request 'Release v0.4.1 : Fix issue with slashes on encoded url strings' (#117) from v0.4.1/fix-encoding-broken-url-issues into develop
Reviewed-on: #117
2024-05-29 19:35:23 +00:00
lamacchinadesiderante a0d1e30eb9 fix issue with slashes on encoded url strings 2024-05-29 21:27:10 +02:00
lamacchinadesiderante af8cbefdf9 Merge pull request 'Add XHamster info in README' (#116) from v0.4.1/xhamster-update-readme into develop
Reviewed-on: #116
2024-05-28 20:47:54 +00:00
La macchina desiderante 3fb99b877c add XHamster info in README 2024-05-28 22:46:47 +02:00
lamacchinadesiderante f311b64d51 Merge pull request 'Release v0.4.1: Refactor platforms and orientation' (#115) from v0.4.1/refactor-platforms-and-orientation into develop
Reviewed-on: #115
2024-05-28 20:23:44 +00:00
La macchina desiderante d41e3a91c2 resolve o/c violation inside Platform component 2024-05-28 22:20:05 +02:00
La macchina desiderante 6a8e433367 resolve o/c violation inside Orientation component 2024-05-28 22:10:35 +02:00
lamacchinadesiderante 97dd3caa26 Merge pull request 'v0.4.1/add-xhamster-support' (#114) from v0.4.1/add-xhamster-support into develop
Reviewed-on: #114
2024-05-28 19:44:14 +00:00
La macchina desiderante 9ede9a6b9b add xhamster video/related scrape 2024-05-28 21:40:32 +02:00
La macchina desiderante 1905707b92 add xhamster gallery search 2024-05-28 20:28:02 +02:00
La macchina desiderante 291c6efe55 add xhamster base structure 2024-05-28 20:10:17 +02:00
lamacchinadesiderante dc3fc541b9 Merge pull request 'Release v0.4.0' (#110) from develop into main
Reviewed-on: #110
2024-05-27 19:05:11 +00:00
lamacchinadesiderante a2e61d83f1 Merge pull request 'store pornhub gallery results into redis only if search is not empty' (#109) from v0.4.0/fix-pornhub-redis-empty-search-issue into develop
Reviewed-on: #109
2024-05-27 10:52:41 +00:00
La macchina desiderante 039f70c086 store pornhub gallery results into redis only if search is not empty 2024-05-27 12:49:01 +02:00
lamacchinadesiderante 120be7734c Merge pull request 'Release v0.4.0: Remove single/double quotes from DISABLED_PLATFORMS variable' (#108) from v0.4.0/fix-disabled-platforms-variable-issues into develop
Reviewed-on: #108
2024-05-27 09:54:18 +00:00
La macchina desiderante 248d71a9dc remove single/double quotes from DISABLED_PLATFORMS variable 2024-05-27 11:53:03 +02:00
lamacchinadesiderante 654fc8cc15 Merge pull request 'Update README.md' (#107) from v0.4.0/add-roadmap-inside-readme into develop
Reviewed-on: #107
2024-05-26 18:40:11 +00:00
lamacchinadesiderante eba5a8a674 Update README.md 2024-05-26 18:39:41 +00:00
lamacchinadesiderante 409ccfc883 Merge pull request 'Release v0.4.0: Add possibility to disable platforms' (#106) from v0.4.0/add-plaform-disabling-support into develop
Reviewed-on: #106
2024-05-26 12:25:05 +00:00
La macchina desiderante 68fc3e3d76 add possibility to disable platforms 2024-05-26 14:22:52 +02:00
lamacchinadesiderante 7e2ddd7a88 Merge pull request 'Release v0.4.0: Fix issue with Plyr.js and displayName' (#105) from v0.4.0/fix-plyrjs-issue-displayname into develop
Reviewed-on: #105
2024-05-26 10:48:25 +00:00
La macchina desiderante 84fc8469c0 fix issue with Plyr.js and displayName 2024-05-26 12:46:30 +02:00
lamacchinadesiderante e9365ca27d Merge pull request 'Release v0.4.0: Rewrite Plyr.js component with useRef' (#104) from v0.4.0/complete-plyrjs-integration into develop
Reviewed-on: #104
2024-05-26 10:38:47 +00:00
La macchina desiderante 38631987ea rewrite Plyr.js component with useRef 2024-05-26 11:56:56 +02:00
lamacchinadesiderante df1ec349c7 Merge pull request 'Release v0.4.0: Add Redtube support' (#103) from v0.4.0/add-redtube-support into develop
Reviewed-on: #103
2024-05-26 09:21:02 +00:00
La macchina desiderante dd8e9838a3 add RedTube video/related scraping 2024-05-26 11:12:52 +02:00
La macchina desiderante 401351289b re-install and add package-lock.json 2024-05-26 10:17:58 +02:00
La macchina desiderante e77e8c4949 add RedTube gallery/search/orientations 2024-05-26 10:08:55 +02:00
La macchina desiderante 18b7c6cb2e add RedTube base structure 2024-05-26 09:21:18 +02:00
La macchina desiderante df9cb431f4 update README with RedTube info 2024-05-26 09:13:12 +02:00
lamacchinadesiderante 1142027946 Merge pull request 'Release v0.4.0: Add YouPorn support' (#102) from v0.4.0/add-youporn-support into develop
Reviewed-on: #102
2024-05-25 18:15:20 +00:00
La macchina desiderante a545a1f4d0 add YouPorn related videos 2024-05-25 20:13:59 +02:00
La macchina desiderante 7c5ca07ab4 add YouPorn video scrape 2024-05-25 20:03:25 +02:00
La macchina desiderante b84ad5127a add YouPorn gallery/search 2024-05-25 19:25:00 +02:00
La macchina desiderante 089dddd385 add YouPorn base structure 2024-05-25 19:01:28 +02:00
La macchina desiderante 92a313aa2c update README with YouPorn / Platform info 2024-05-25 18:47:14 +02:00
lamacchinadesiderante 82c54f63da Merge pull request 'Release v0.4.0: Add opacity toggler for desktop' (#101) from v0.4.0/add-opacity-toggler-for-desktop into develop
Reviewed-on: #101
2024-05-25 15:16:23 +00:00
La macchina desiderante cef782bef9 add opacity toggler for desktop 2024-05-25 17:14:01 +02:00
lamacchinadesiderante f5bf29285f Merge pull request 'Release v0.4.0: Fix skeleton loading for imgs' (#100) from v0.4.0/add-loading-skeletons into develop
Reviewed-on: #100
2024-05-25 14:52:20 +00:00
La macchina desiderante 62cfbd4407 fix skeleton loading for imgs 2024-05-25 15:17:44 +02:00
lamacchinadesiderante e09e8d2f15 Merge pull request 'Release 0.4: using Plyr.js for mp4 and Video.js for hls' (#99) from v0.4.0/migrate-to-plyr-js into develop
Reviewed-on: #99
2024-05-25 13:05:50 +00:00
La macchina desiderante 0b1f549354 using Plyr.js for mp4 and Video.js for hls 2024-05-25 14:55:46 +02:00
lamacchinadesiderante 0c450ff019 Merge pull request 'Release v0.4: Add PornHub support / Server-side streaming / Plyr.js / Video srcset' (#96) from feature/pornhub-support into develop
Reviewed-on: #96
2024-05-25 11:46:41 +00:00
La macchina desiderante ac352db747 Merge branch 'develop' into feature/pornhub-support 2024-05-25 13:45:48 +02:00
La macchina desiderante 69dd722514 update README with encoding key details 2024-05-24 23:30:30 +02:00
La macchina desiderante 702b67bdd7 add url encoding/decoding mechanism 2024-05-24 23:09:56 +02:00
La macchina desiderante 7b1d712904 fix streaming issue with Safari 2024-05-24 22:37:32 +02:00
La macchina desiderante b0710671b3 add PornHub orientations and switching mechanism 2024-05-24 21:45:42 +02:00
La macchina desiderante 6efe6582a5 add warning box for PornHub support 2024-05-24 20:44:54 +02:00
lamacchinadesiderante b1721f899b Merge pull request 'v0.3.2/improve-dockerfile' (#95) from v0.3.2/improve-dockerfile into main
Reviewed-on: #95
2024-05-24 14:55:13 +00:00
lamacchinadesiderante 69d68d4012 Merge pull request 'Release v0.3.2' (#94) from v0.3.2/improve-dockerfile into develop
Reviewed-on: #94
2024-05-24 14:54:21 +00:00
lamacchinadesiderante e71fd52be6 update version number 2024-05-24 16:51:25 +02:00
Zottelchen 94bb688bca optimnize Dockerfile 2024-05-24 16:44:30 +02:00
La macchina desiderante af169db4df add related video gallery 2024-05-23 23:42:04 +02:00
La macchina desiderante ee799c0424 remove plyr safari controls 2024-05-23 21:47:27 +02:00
La macchina desiderante c6abf4defd set plyr default options 2024-05-23 21:05:23 +02:00
La macchina desiderante 6cac83323d reduce Redis expiry for pornhub videos 2024-05-23 18:37:44 +02:00
La macchina desiderante 3a407c7c92 add color styling for Plyr (according to theme) 2024-05-23 18:13:22 +02:00
La macchina desiderante 81b3bb8660 add redis storage for pornhub 2024-05-21 23:29:56 +02:00
La macchina desiderante 2c1d71e483 update version number to 0.4.0 2024-05-21 22:16:00 +02:00
La macchina desiderante f2cc39adae merge branch develop into feature/pornhub-support 2024-05-21 22:14:54 +02:00
La macchina desiderante 1ad9b46ea3 complete server-side streaming 2024-05-21 22:08:55 +02:00
La macchina desiderante d8af939e31 add Plyr.js and working multisource client-side stream 2024-05-20 23:10:22 +02:00
La macchina desiderante f5908b9a5d add (client-side only) pornhub video streaming (WIP) 2024-05-20 20:52:02 +02:00
lamacchinadesiderante 1a6960f610 Merge pull request 'Release v0.3.1' (#91) from develop into main
Reviewed-on: #91
2024-05-19 17:19:56 +00:00
lamacchinadesiderante 6fc56eb7d3 Merge pull request 'v0.3.1/reduce-redis-content-expiry' (#90) from v0.3.1/reduce-redis-content-expiry into develop
Reviewed-on: #90
2024-05-19 17:18:12 +00:00
La macchina desiderante 7fc09b165e update node alpine version 2024-05-19 11:11:12 +02:00
La macchina desiderante ca1d5dfec1 reduce redis content expiry to 1 hour 2024-05-19 11:00:06 +02:00
La macchina desiderante b1dc47d461 add pornhub support (WIP - not working yet) 2024-05-19 10:44:10 +02:00
lamacchinadesiderante 2283609af0 Merge pull request 'Release v0.3 - Add Redis support' (#82) from develop into main
Reviewed-on: #82
2024-05-18 11:35:06 +00:00
lamacchinadesiderante c7e9305588 Merge branch 'main' into develop 2024-05-18 11:33:27 +00:00
lamacchinadesiderante 0650c995a7 Merge pull request 'v0.3/redis-setup' (#80) from v0.3/redis-setup into develop
Reviewed-on: #80
2024-05-18 11:32:13 +00:00
La macchina desiderante b69a7d9751 remove spaces from README 2024-05-18 12:42:45 +02:00
La macchina desiderante 69d587cfb3 update readme and .env.example 2024-05-18 12:41:16 +02:00
La macchina desiderante 15deeddd14 fix caching issue with no related data 2024-05-18 11:50:19 +02:00
La macchina desiderante 5245125097 load env variables directly from docker compose 2024-05-17 12:02:55 +02:00
La macchina desiderante 5a149ebe26 fix dockerfile and package-json BREAKING issues 2024-05-17 11:43:07 +02:00
La macchina desiderante 4f15f44e12 add redis integration gitignore (WIP) 2024-05-17 02:25:27 +02:00
La macchina desiderante a1d751e0bb add redis integration (WIP) 2024-05-17 02:24:41 +02:00
lamacchinadesiderante 4ceb007e25 Merge pull request 'Add blacklist warning to readme' (#79) from develop into main
Reviewed-on: #79
2024-05-16 15:40:13 +00:00
lamacchinadesiderante a509e41ad7 Merge pull request 'update readme' (#78) from v0.2.1/add-readme-warning into develop
Reviewed-on: #78
2024-05-16 15:39:24 +00:00
lamacchinadesiderante 39c9fe5a09 update readme 2024-05-16 17:38:48 +02:00
60 changed files with 2573 additions and 405 deletions

View File

@ -2,4 +2,6 @@ node_modules
.git .git
.gitignore .gitignore
.next .next
package-lock.json package-lock.json
.env

20
.env.example Normal file
View File

@ -0,0 +1,20 @@
# enable content cache (less server api calls)
ENABLE_REDIS=true
# if cache enabled, sets redis url for Docker network (see docker-compose.yaml)
# REDIS_URL='redis://redis:6379'
# if cache enabled, set redis url for external redis
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=''

3
.gitignore vendored
View File

@ -34,3 +34,6 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
# local .env
.env

View File

@ -1,21 +1,28 @@
# PHASE 1: copy and build # PHASE 1: copy and build
FROM node:alpine 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:alpine 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"]

View File

@ -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,27 +21,88 @@ 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.
Project requires no configuration. ## IMPORTANT: encoding key generation:
You can run it via Docker with docker-compose by opening root folder via console and running: 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
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
``` ```
And head browser to `localhost:8069`. And head browser to `localhost:8069`.
Or you can run it outside Docker via npm (tested with NodeJS `20.11`) by opening root folder via console and running: ### Caching
Starting from version `0.3.0` caching is enabled by default inside `docker-compose.yaml`.
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:`.
### 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
You can also run project outside Docker via npm (tested with NodeJS `20.11` and above).
You can run the project by opening root folder via console and running:
``` ```
npm install npm install
npm run build npm run build
npm run start 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
If you want to enable caching, please rename `.env.example` to `.env` file inside root folder. Inside `.env` file you will find following variables:
```
ENABLE_REDIS=true
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.
# 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:
@ -51,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
@ -60,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'
```

View File

@ -19,3 +19,14 @@ services:
ports: ports:
- "8069:3000" - "8069:3000"
environment:
- ENABLE_REDIS=true
- 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:
image: redis:alpine
container_name: redis
restart: unless-stopped

View File

@ -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 :("
} }
} }

View File

@ -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 :("
} }
} }

1162
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "proxyraye-next", "name": "proxyraye-next",
"version": "0.2.0", "version": "0.4.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
@ -9,31 +9,34 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@picocss/pico": "^2.0.6", "@picocss/pico": "2.0.6",
"@reduxjs/toolkit": "^2.2.3", "@reduxjs/toolkit": "2.2.3",
"axios": "^1.6.8", "axios": "1.6.8",
"cheerio": "^1.0.0-rc.12", "cheerio": "1.0.0-rc.12",
"classnames": "^2.5.1", "classnames": "2.5.1",
"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",
"react": "^18", "plyr-react": "5.3.0",
"react-cookie": "^7.1.4", "react": "18.3.0",
"react-dom": "^18", "react-cookie": "7.1.4",
"react-icons": "^5.1.0", "react-dom": "18.3.0",
"react-image": "^4.1.0", "react-icons": "5.1.0",
"react-redux": "^9.1.1", "react-image": "4.1.0",
"redux-persist": "^6.0.0", "react-redux": "9.1.1",
"video.js": "^8.10.0", "redis": "4.6.14",
"videojs-hls-quality-selector": "^2.0.0" "redux-persist": "6.0.0",
"video.js": "8.12.0",
"videojs-hls-quality-selector": "2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "20.12.12",
"@types/react": "^18", "@types/react": "18.3.0",
"@types/react-dom": "^18", "@types/react-dom": "18.3.0",
"eslint": "^8", "@types/redis": "4.0.11",
"eslint": "8",
"eslint-config-next": "14.2.2", "eslint-config-next": "14.2.2",
"sass": "^1.75.0", "sass": "1.75.0",
"typescript": "^5" "typescript": "5.4.5"
} }
} }

View File

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

View File

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

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

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

View File

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

View File

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

View File

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

View File

@ -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>

View File

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

View File

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

View File

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

View File

@ -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} />}
</> </>

View File

@ -0,0 +1 @@
export const DEFAULT_ENCODING_KEY = 'oom2oz8ut0ieshie1Hae'

21
src/constants/redis.ts Normal file
View File

@ -0,0 +1,21 @@
const EX_MIN = 60
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/'

1
src/constants/stream.ts Normal file
View File

@ -0,0 +1 @@
export const DEFAULT_VIDEO_STREAM_ROUTE_PREFIX = '/api/stream'

View File

@ -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/'

View File

@ -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 {

View File

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

38
src/redis/client.ts Normal file
View File

@ -0,0 +1,38 @@
import { createClient } from 'redis';
export const getDataFromRedis = async (key: string): Promise<Object | null> => {
if (Boolean(process.env.ENABLE_REDIS)) {
const redis = await createClient({ url: process.env.REDIS_URL })
.on('error', err => console.log('Redis Client Error', err))
.connect()
const cached = await redis.get(key);
if (cached) {
await redis.disconnect();
return JSON.parse(cached)
} else {
return null
}
} else {
return null
}
}
export const storeDataIntoRedis = async (key: string, data: Object, expiry?: Object) => {
if (Boolean(process.env.ENABLE_REDIS)) {
const redis = await createClient({ url: process.env.REDIS_URL })
.on('error', err => console.log('Redis Client Error', err))
.connect()
await redis.set(key, JSON.stringify(data), expiry);
await redis.disconnect();
}
}

View File

@ -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 {

13
src/utils/platforms.ts Normal file
View File

@ -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)]
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 ]
}

View File

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

View File

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

View File

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

View File

@ -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 ]
}

View File

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

View File

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

View File

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

View File

@ -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 ]
}

View File

@ -1,5 +1,5 @@
import { FetchParams, GalleryData, VideoAgent, VideoData } from "@/meta/data"; import { FetchParams, GalleryData, VideoAgent, VideoData } from "@/meta/data";
import { fetchXNXXGalleryData, } from "./gallery"; import { fetchXNXXGalleryData } from "./gallery";
import { fetchXNXXVideoData } from "./video"; import { fetchXNXXVideoData } from "./video";
export class XNXXAgent implements VideoAgent { export class XNXXAgent implements VideoAgent {

View File

@ -8,6 +8,8 @@ import { getXNXXQueryUrl } from './url';
import { Platforms } from '@/meta/settings'; import { Platforms } from '@/meta/settings';
import { XNXX_BASE_URL } from '@/constants/urls'; import { XNXX_BASE_URL } from '@/constants/urls';
import { getDataFromRedis, storeDataIntoRedis } from '@/redis/client';
import { DEFAULT_XNXX_CONTENT_EXPIRY } from '@/constants/redis';
export const fetchXNXXGalleryData = async (params?: FetchParams): Promise<GalleryData[]> => { export const fetchXNXXGalleryData = async (params?: FetchParams): Promise<GalleryData[]> => {
@ -17,9 +19,15 @@ export const fetchXNXXGalleryData = async (params?: FetchParams): Promise<Galler
const queryUrl = await getXNXXQueryUrl(params?.query) const queryUrl = await getXNXXQueryUrl(params?.query)
const cachedData = await getDataFromRedis(queryUrl)
if (cachedData) {
return cachedData as GalleryData[]
}
await axios.get(queryUrl, reqHeaders) await axios.get(queryUrl, reqHeaders)
.then(response => { .then(async response => {
const html = response.data; const html = response.data;
@ -41,6 +49,8 @@ export const fetchXNXXGalleryData = async (params?: FetchParams): Promise<Galler
}) })
}) })
await storeDataIntoRedis(queryUrl, data, DEFAULT_XNXX_CONTENT_EXPIRY);
}).catch((error: AxiosError) => { }).catch((error: AxiosError) => {
// handle errors // handle errors
}); });

View File

@ -8,11 +8,14 @@ import * as cheerio from "cheerio";
import { Platforms } from '@/meta/settings'; import { Platforms } from '@/meta/settings';
import { findRelatedVideos, findVideoUrlInsideTagStringByFunctionNameAndExtension } from '@/utils/scrape/common/wgcz'; import { findRelatedVideos, findVideoUrlInsideTagStringByFunctionNameAndExtension } from '@/utils/scrape/common/wgcz';
import { getHeaders } from '@/utils/scrape/common/headers'; import { getHeaders } from '@/utils/scrape/common/headers';
import { DEFAULT_RELATED_VIDEO_KEY_PATH, DEFAULT_XNXX_CONTENT_EXPIRY } from '@/constants/redis';
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[] = [];
@ -23,9 +26,16 @@ export const fetchXNXXVideoData = async (videoId: string, params?: FetchParams):
const queryUrl = `${host}${videoId}` const queryUrl = `${host}${videoId}`
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) await axios.get(queryUrl, reqHeaders)
.then(response => { .then(async response => {
const html = response.data; const html = response.data;
@ -41,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) {
@ -54,6 +72,8 @@ export const fetchXNXXVideoData = async (videoId: string, params?: FetchParams):
}) })
await storeDataIntoRedis(queryUrl, data, DEFAULT_XNXX_CONTENT_EXPIRY);
// populate related gallery // populate related gallery
scriptTags.map((idx, elem) => { scriptTags.map((idx, elem) => {
const relatedVideos = findRelatedVideos($(elem).toString(), Platforms.xnxx) const relatedVideos = findRelatedVideos($(elem).toString(), Platforms.xnxx)
@ -63,6 +83,8 @@ export const fetchXNXXVideoData = async (videoId: string, params?: FetchParams):
} }
}) })
await storeDataIntoRedis(queryUrl + DEFAULT_RELATED_VIDEO_KEY_PATH, related, DEFAULT_XNXX_CONTENT_EXPIRY);
}).catch((error: AxiosError) => { }).catch((error: AxiosError) => {
// handle errors // handle errors
}); });

View File

@ -6,30 +6,40 @@ import { getHeaders } from '@/utils/scrape/common/headers';
import { getXVideosQueryUrl } from './url'; import { getXVideosQueryUrl } from './url';
import { Platforms } from '@/meta/settings'; import { Platforms } from '@/meta/settings';
import { getDataFromRedis, storeDataIntoRedis } from '@/redis/client';
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)
const cachedData = await getDataFromRedis(queryUrl)
if (cachedData) {
return cachedData as GalleryData[]
}
await axios.get(queryUrl, reqHeaders) await axios.get(queryUrl, reqHeaders)
.then(response => { .then(async response => {
const html = response.data; const html = response.data;
const $ = cheerio.load(html); const $ = cheerio.load(html);
const thumbs = $(".thumb-block"); const thumbs = $(".thumb-block");
thumbs.map((key, thumb) => { thumbs.map((key, thumb) => {
const videoUrl = $(thumb).find(".thumb a").attr("href") const videoUrl = $(thumb).find(".thumb a").attr("href")
const imgUrl = $(thumb).find(".thumb img").attr("data-src") const imgUrl = $(thumb).find(".thumb img").attr("data-src")
const text = $(thumb).find(".thumb-under a").attr("title") const text = $(thumb).find(".thumb-under a").attr("title")
videoUrl && imgUrl && text && data.push({ videoUrl && imgUrl && text && data.push({
videoUrl, videoUrl,
imgUrl, imgUrl,
@ -38,7 +48,10 @@ export const fetchXVideosGalleryData = async (params?: FetchParams): Promise<Gal
}) })
}) })
}).catch((error: AxiosError) => { await storeDataIntoRedis(queryUrl, data, DEFAULT_XVIDEOS_CONTENT_EXPIRY);
})
.catch((error: AxiosError) => {
// handle errors // handle errors
}); });

View File

@ -8,11 +8,14 @@ import { getHeaders } from '@/utils/scrape/common/headers';
import { Platforms } from '@/meta/settings'; import { Platforms } from '@/meta/settings';
import { findRelatedVideos, findVideoUrlInsideTagStringByFunctionNameAndExtension } from '@/utils/scrape/common/wgcz'; import { findRelatedVideos, findVideoUrlInsideTagStringByFunctionNameAndExtension } from '@/utils/scrape/common/wgcz';
import { getDataFromRedis, storeDataIntoRedis } from '@/redis/client';
import { DEFAULT_RELATED_VIDEO_KEY_PATH, DEFAULT_XVIDEOS_CONTENT_EXPIRY } from '@/constants/redis';
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[] = [];
@ -23,9 +26,16 @@ export const fetchXvideosVideoData = async (videoId: string, params?: FetchParam
const queryUrl = `${host}${videoId}` const queryUrl = `${host}${videoId}`
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) await axios.get(queryUrl, reqHeaders)
.then(response => { .then(async response => {
const html = response.data; const html = response.data;
@ -41,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) {
@ -54,6 +72,8 @@ export const fetchXvideosVideoData = async (videoId: string, params?: FetchParam
}) })
await storeDataIntoRedis(queryUrl, data, DEFAULT_XVIDEOS_CONTENT_EXPIRY);
// populate related gallery // populate related gallery
scriptTags.map((idx, elem) => { scriptTags.map((idx, elem) => {
const relatedVideos = findRelatedVideos($(elem).toString(), Platforms.xvideos) const relatedVideos = findRelatedVideos($(elem).toString(), Platforms.xvideos)
@ -63,6 +83,8 @@ export const fetchXvideosVideoData = async (videoId: string, params?: FetchParam
} }
}) })
await storeDataIntoRedis(queryUrl + DEFAULT_RELATED_VIDEO_KEY_PATH, related, DEFAULT_XVIDEOS_CONTENT_EXPIRY);
}).catch((error: AxiosError) => { }).catch((error: AxiosError) => {
// handle errors // handle errors
}); });

View File

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

View File

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

View File

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

View File

@ -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 ]
}

View File

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