complete basic functionalities (#1)
Questa PR include il layout e le funzionalità di base (ricerca e visione video SD/HD). Reviewed-on: #1 Co-authored-by: lamacchinadesiderante <lamacchinadesiderante@tuta.io> Co-committed-by: lamacchinadesiderante <lamacchinadesiderante@tuta.io>
This commit is contained in:
parent
04a4684f6f
commit
607acbb875
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"Header": {
|
||||
"title": "Proxy Raye",
|
||||
"description": "A proxy for XVideos",
|
||||
"disclaimer_0": "Genital sexuality is only one of the many possible conceptions of sexuality",
|
||||
"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?"
|
||||
},
|
||||
"Search": {
|
||||
"placeholder": "categories, pornostars, etc...",
|
||||
"submit": "Search"
|
||||
},
|
||||
"Results": {
|
||||
"query": "Search results for: {{ query }}",
|
||||
"toggle": "Show preview",
|
||||
"noData": "No videos found :("
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"Header": {
|
||||
"title": "Proxy Raye",
|
||||
"description": "Un proxy per XVideos",
|
||||
"disclaimer_0": "Quella genitale è solo una delle possibili concezioni della sessualità.",
|
||||
"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?"
|
||||
},
|
||||
"Search": {
|
||||
"placeholder": "categorie, pornostar, ecc...",
|
||||
"submit": "Cerca"
|
||||
},
|
||||
"Results": {
|
||||
"query": "Risultati della ricerca per: {{ query }}",
|
||||
"toggle": "Mostra anteprime risultati",
|
||||
"noData": "Nessun video trovato :("
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
const createNextIntlPlugin = require('next-intl/plugin');
|
||||
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
const path = require('path')
|
||||
|
||||
module.exports = withNextIntl({
|
||||
sassOptions: {
|
||||
includePaths: [path.join(__dirname, 'src/styles')],
|
||||
},
|
||||
})
|
|
@ -1,4 +0,0 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
|
@ -9,16 +9,24 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@picocss/pico": "^2.0.6",
|
||||
"axios": "^1.6.8",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"classnames": "^2.5.1",
|
||||
"next": "14.2.2",
|
||||
"next-intl": "^3.11.3",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"next": "14.2.2"
|
||||
"video.js": "^8.10.0",
|
||||
"videojs-hls-quality-selector": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.2"
|
||||
"eslint-config-next": "14.2.2",
|
||||
"sass": "^1.75.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import "@/styles/globals.scss"
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
params: {locale}
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
params: {locale: string};
|
||||
}>) {
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Proxy Raye: un proxy per XVideos basato su PornInvidious</title>
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import axios from 'axios';
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
|
||||
import Layout from "@/components/Layout";
|
||||
|
||||
import Home from "@/components/Pages/Home";
|
||||
|
||||
import { fetchGalleryData } from '@/utils/scrape/gallery';
|
||||
|
||||
export default async function HomePage() {
|
||||
|
||||
const data = await fetchGalleryData()
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Home data={data} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import Layout from "@/components/Layout";
|
||||
|
||||
import Search from "@/components/Pages/Search";
|
||||
|
||||
import { fetchGalleryData } from "@/utils/scrape/gallery";
|
||||
|
||||
export default async function SearchPage({ params }: { params: { query: string } }) {
|
||||
|
||||
const data = await fetchGalleryData({ query: params.query })
|
||||
|
||||
return <Layout>
|
||||
<Search data={data} query={params.query} />
|
||||
</Layout>
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import Layout from "@/components/Layout";
|
||||
|
||||
import Search from "@/components/Pages/Search";
|
||||
|
||||
export default async function SearchNoQueryPage() {
|
||||
|
||||
return <Layout>
|
||||
<Search data={[]} query={''} />
|
||||
</Layout>
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { redirect } from 'next/navigation';
|
||||
|
||||
import Layout from "@/components/Layout";
|
||||
|
||||
import Video from "@/components/Pages/Video";
|
||||
|
||||
import { fetchVideoData } from "@/utils/scrape/video";
|
||||
|
||||
import { useLocale } from 'next-intl';
|
||||
|
||||
export default async function VideoPage({ params }: { params: { id: string } }) {
|
||||
|
||||
const locale = useLocale()
|
||||
|
||||
const decodedId = decodeURIComponent(params.id)
|
||||
|
||||
const [data, related] = await fetchVideoData(decodedId)
|
||||
|
||||
if (!data.lowResUrl) {
|
||||
redirect(`/${locale}/404`)
|
||||
}
|
||||
|
||||
return <Layout>
|
||||
<Video id={params.id} data={data} related={related}/>
|
||||
</Layout>
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
|
@ -1,107 +0,0 @@
|
|||
:root {
|
||||
--max-width: 1100px;
|
||||
--border-radius: 12px;
|
||||
--font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
|
||||
"Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
|
||||
"Fira Mono", "Droid Sans Mono", "Courier New", monospace;
|
||||
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
|
||||
--primary-glow: conic-gradient(
|
||||
from 180deg at 50% 50%,
|
||||
#16abff33 0deg,
|
||||
#0885ff33 55deg,
|
||||
#54d6ff33 120deg,
|
||||
#0071ff33 160deg,
|
||||
transparent 360deg
|
||||
);
|
||||
--secondary-glow: radial-gradient(
|
||||
rgba(255, 255, 255, 1),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
|
||||
--tile-start-rgb: 239, 245, 249;
|
||||
--tile-end-rgb: 228, 232, 233;
|
||||
--tile-border: conic-gradient(
|
||||
#00000080,
|
||||
#00000040,
|
||||
#00000030,
|
||||
#00000020,
|
||||
#00000010,
|
||||
#00000010,
|
||||
#00000080
|
||||
);
|
||||
|
||||
--callout-rgb: 238, 240, 241;
|
||||
--callout-border-rgb: 172, 175, 176;
|
||||
--card-rgb: 180, 185, 188;
|
||||
--card-border-rgb: 131, 134, 135;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
|
||||
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
|
||||
--secondary-glow: linear-gradient(
|
||||
to bottom right,
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0.3)
|
||||
);
|
||||
|
||||
--tile-start-rgb: 2, 13, 46;
|
||||
--tile-end-rgb: 2, 5, 19;
|
||||
--tile-border: conic-gradient(
|
||||
#ffffff80,
|
||||
#ffffff40,
|
||||
#ffffff30,
|
||||
#ffffff20,
|
||||
#ffffff10,
|
||||
#ffffff10,
|
||||
#ffffff80
|
||||
);
|
||||
|
||||
--callout-rgb: 20, 20, 20;
|
||||
--callout-border-rgb: 108, 108, 108;
|
||||
--card-rgb: 100, 100, 100;
|
||||
--card-border-rgb: 200, 200, 200;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
|
@ -1,230 +0,0 @@
|
|||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: inherit;
|
||||
justify-content: inherit;
|
||||
align-items: inherit;
|
||||
font-size: 0.85rem;
|
||||
max-width: var(--max-width);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.description a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.description p {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
background-color: rgba(var(--callout-rgb), 0.5);
|
||||
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.code {
|
||||
font-weight: 700;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(25%, auto));
|
||||
max-width: 100%;
|
||||
width: var(--max-width);
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: var(--border-radius);
|
||||
background: rgba(var(--card-rgb), 0);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0);
|
||||
transition: background 200ms, border 200ms;
|
||||
}
|
||||
|
||||
.card span {
|
||||
display: inline-block;
|
||||
transition: transform 200ms;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
opacity: 0.6;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
max-width: 30ch;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
background: var(--secondary-glow);
|
||||
border-radius: 50%;
|
||||
width: 480px;
|
||||
height: 360px;
|
||||
margin-left: -400px;
|
||||
}
|
||||
|
||||
.center::after {
|
||||
background: var(--primary-glow);
|
||||
width: 240px;
|
||||
height: 180px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.center::before,
|
||||
.center::after {
|
||||
content: "";
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
filter: blur(45px);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: relative;
|
||||
}
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.card:hover {
|
||||
background: rgba(var(--card-rgb), 0.1);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0.15);
|
||||
}
|
||||
|
||||
.card:hover span {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.card:hover span {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 700px) {
|
||||
.content {
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
margin-bottom: 120px;
|
||||
max-width: 320px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 2.5rem;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.center {
|
||||
padding: 8rem 0 6rem;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
transform: none;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.description a {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.description p,
|
||||
.description div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.description p {
|
||||
align-items: center;
|
||||
inset: 0 0 auto;
|
||||
padding: 2rem 1rem 1.4rem;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--background-start-rgb), 1),
|
||||
rgba(var(--callout-rgb), 0.5)
|
||||
);
|
||||
background-clip: padding-box;
|
||||
backdrop-filter: blur(24px);
|
||||
}
|
||||
|
||||
.description div {
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
inset: auto 0 0;
|
||||
padding: 2rem;
|
||||
height: 200px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
rgb(var(--background-end-rgb)) 40%
|
||||
);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet and Smaller Desktop */
|
||||
@media (min-width: 701px) and (max-width: 1120px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 50%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.vercelLogo {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
import Image from "next/image";
|
||||
import styles from "./page.module.css";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.description}>
|
||||
<p>
|
||||
Get started by editing
|
||||
<code className={styles.code}>src/app/page.tsx</code>
|
||||
</p>
|
||||
<div>
|
||||
<a
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
By{" "}
|
||||
<Image
|
||||
src="/vercel.svg"
|
||||
alt="Vercel Logo"
|
||||
className={styles.vercelLogo}
|
||||
width={100}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.center}>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/next.svg"
|
||||
alt="Next.js Logo"
|
||||
width={180}
|
||||
height={37}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.grid}>
|
||||
<a
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2>
|
||||
Docs <span>-></span>
|
||||
</h2>
|
||||
<p>Find in-depth information about Next.js features and API.</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2>
|
||||
Learn <span>-></span>
|
||||
</h2>
|
||||
<p>Learn about Next.js in an interactive course with quizzes!</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2>
|
||||
Templates <span>-></span>
|
||||
</h2>
|
||||
<p>Explore starter templates for Next.js.</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2>
|
||||
Deploy <span>-></span>
|
||||
</h2>
|
||||
<p>
|
||||
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
const Description: React.FC = () => {
|
||||
|
||||
const t = useTranslations('Header');
|
||||
|
||||
return (
|
||||
<div>{t('description')}</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Description;
|
|
@ -0,0 +1,10 @@
|
|||
@import 'colors';
|
||||
@import 'spacing';
|
||||
|
||||
.messageBox {
|
||||
border: 1px solid $colors_yellow;
|
||||
text-align: center;
|
||||
padding: $spacing_8;
|
||||
border-radius: $spacing_4;
|
||||
margin-bottom: $spacing_32;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
|
||||
import style from './Disclaimer.module.scss'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const Disclaimer: React.FC = () => {
|
||||
|
||||
const MAX_DISCLAIMER_NO = 6
|
||||
|
||||
const t = useTranslations('Header');
|
||||
|
||||
const getRandomArbitrary = (max: number) => {
|
||||
return Math.floor( Math.random() * max);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={style.messageBox}>{t(`disclaimer_${getRandomArbitrary(MAX_DISCLAIMER_NO)}`)}</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Disclaimer;
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
import {useTranslations} from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
|
||||
const Title: React.FC = () => {
|
||||
|
||||
const t = useTranslations('Header');
|
||||
|
||||
return (
|
||||
<Link href={'/'}><h1>{t('title')}</h1></Link>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default Title;
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
|
||||
import Title from './Title';
|
||||
import Description from './Description';
|
||||
import Disclaimer from './Disclaimer';
|
||||
|
||||
const Header: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title />
|
||||
<Description />
|
||||
<Disclaimer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
|
@ -0,0 +1,5 @@
|
|||
@import 'spacing';
|
||||
|
||||
.container {
|
||||
margin-bottom: $spacing_32;
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import videojs from 'video.js';
|
||||
|
||||
import 'video.js/dist/video-js.css';
|
||||
|
||||
export const VideoJS = (props: { options: any; onReady: any; }) => {
|
||||
const videoRef = React.useRef(null);
|
||||
const playerRef = React.useRef(null);
|
||||
const {options, onReady} = props;
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
// Make sure Video.js player is only initialized once
|
||||
if (!playerRef.current) {
|
||||
// The Video.js player needs to be _inside_ the component el for React 18 Strict Mode.
|
||||
const videoElement = document.createElement("video-js");
|
||||
|
||||
videoElement.classList.add('vjs-big-play-centered');
|
||||
//@ts-ignore
|
||||
videoRef.current.appendChild(videoElement);
|
||||
|
||||
//@ts-ignore
|
||||
const player = playerRef.current = videojs(videoElement, options, () => {
|
||||
videojs.log('player is ready');
|
||||
onReady && onReady(player);
|
||||
});
|
||||
|
||||
// You could update an existing player in the `else` block here
|
||||
// on prop change, for example:
|
||||
} else {
|
||||
const player = playerRef.current;
|
||||
|
||||
//@ts-ignore
|
||||
player.autoplay(options.autoplay);
|
||||
|
||||
//@ts-ignore
|
||||
player.src(options.sources);
|
||||
}
|
||||
}, [options, videoRef]);
|
||||
|
||||
// Dispose the Video.js player when the functional component unmounts
|
||||
React.useEffect(() => {
|
||||
const player = playerRef.current;
|
||||
|
||||
return () => {
|
||||
//@ts-ignore
|
||||
if (player && !player.isDisposed()) {
|
||||
//@ts-ignore
|
||||
player.dispose();
|
||||
playerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [playerRef]);
|
||||
|
||||
return (
|
||||
<div data-vjs-player>
|
||||
<div ref={videoRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default VideoJS;
|
|
@ -0,0 +1,63 @@
|
|||
'use client'
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import style from './Player.module.scss'
|
||||
|
||||
import VideoJS from './VideoJS';
|
||||
import { VideoData } from '@/meta/data';
|
||||
|
||||
import 'videojs-hls-quality-selector';
|
||||
|
||||
interface Props {
|
||||
data: VideoData
|
||||
}
|
||||
|
||||
const Player: React.FC<Props> = (props) => {
|
||||
|
||||
const { data } = props;
|
||||
|
||||
const videoSrc = data.hlsUrl ?? data.lowResUrl
|
||||
const videoType = data.hlsUrl ? 'application/x-mpegURL' : 'video/mp4'
|
||||
|
||||
const playerRef = React.useRef(null);
|
||||
|
||||
const videoJsOptions = {
|
||||
autoplay: true,
|
||||
controls: true,
|
||||
responsive: true,
|
||||
fluid: true,
|
||||
sources: [{
|
||||
src: videoSrc,
|
||||
type: videoType
|
||||
}]
|
||||
};
|
||||
|
||||
//@ts-ignore
|
||||
const handlePlayerReady = (player) => {
|
||||
playerRef.current = player;
|
||||
|
||||
player.hlsQualitySelector({
|
||||
vjsIconClass: "vjs-icon-hd"
|
||||
})
|
||||
|
||||
// You can handle player events here, for example:
|
||||
player.on('waiting', () => {
|
||||
console.log('player is waiting');
|
||||
});
|
||||
|
||||
player.on('dispose', () => {
|
||||
console.log('player will dispose');
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className={style.container}>
|
||||
<VideoJS options={videoJsOptions} onReady={handlePlayerReady} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Player;
|
|
@ -0,0 +1,31 @@
|
|||
@import 'spacing';
|
||||
@import 'breakpoints';
|
||||
|
||||
.galleryContainer {
|
||||
display: grid;
|
||||
gap: $spacing_16;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
@media only screen and (min-width: $portrait) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $tablet) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $laptop) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $desktop) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.galleryContainer.show {
|
||||
@media only screen and (max-width: $portrait) {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
@import 'spacing';
|
||||
@import 'colors';
|
||||
@import 'breakpoints';
|
||||
|
||||
.text {
|
||||
font-size: 12px;
|
||||
color: $colors_yellow;
|
||||
text-decoration: none;
|
||||
|
||||
display: none;
|
||||
|
||||
@media only screen and (min-width: $tablet) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
border-radius: $spacing_4;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.thumbnailContainer {
|
||||
opacity: 0.1;
|
||||
transition: 1s;
|
||||
|
||||
a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnailContainer:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.thumbnailContainer.show {
|
||||
opacity: 1;
|
||||
|
||||
.text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
|
||||
@media only screen and (min-width: $tablet) {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
'use client'
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import Link from 'next/link'
|
||||
|
||||
import style from './Thumbnail.module.scss'
|
||||
|
||||
interface Props {
|
||||
locale: string
|
||||
videoUrl: string
|
||||
imgUrl: string
|
||||
text: string
|
||||
show: boolean
|
||||
}
|
||||
|
||||
const Thumbnail: React.FC<Props> = (props) => {
|
||||
|
||||
const { locale, videoUrl, imgUrl, text, show } = props
|
||||
|
||||
const encodedUri = encodeURIComponent(videoUrl)
|
||||
|
||||
return (
|
||||
<div className={classNames(style.thumbnailContainer, { [style.show]: show } )}>
|
||||
<Link href={`/${locale}/video/${encodedUri}`}>
|
||||
<img className={style.image} src={imgUrl} />
|
||||
<div className={style.text}>{text}</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Thumbnail;
|
|
@ -0,0 +1,38 @@
|
|||
'use client'
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import style from './Gallery.module.scss'
|
||||
import Thumbnail from './Thumbnail';
|
||||
import { GalleryData } from '@/meta/data';
|
||||
|
||||
interface Props {
|
||||
data: GalleryData[]
|
||||
show: boolean
|
||||
locale: string
|
||||
}
|
||||
|
||||
const Gallery: React.FC<Props> = (props) => {
|
||||
|
||||
const { show, locale, data } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classNames(style.galleryContainer, { [style.show]: show })}>
|
||||
{data && data.map((elem, key) => {
|
||||
return <Thumbnail
|
||||
show={show}
|
||||
key={key}
|
||||
imgUrl={elem.imgUrl}
|
||||
videoUrl={elem.videoUrl}
|
||||
text={elem.text}
|
||||
locale={locale} />
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Gallery;
|
|
@ -0,0 +1,22 @@
|
|||
'use client'
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import style from './NoData.module.scss';
|
||||
|
||||
interface Props {
|
||||
msg: string
|
||||
}
|
||||
|
||||
const NoData: React.FC<Props> = (props) => {
|
||||
|
||||
const { msg } = props;
|
||||
|
||||
return (
|
||||
<div className={style.container}>
|
||||
<span>{msg}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoData;
|
|
@ -0,0 +1,7 @@
|
|||
@import 'breakpoints';
|
||||
|
||||
.toggleContainer {
|
||||
@media only screen and (min-width: $tablet) {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
'use client'
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import style from './Toggle.module.scss'
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
handleClick: () => void
|
||||
}
|
||||
|
||||
const Toggle: React.FC<Props> = (props) => {
|
||||
|
||||
const { label, handleClick } = props
|
||||
|
||||
return (
|
||||
<div className={style.toggleContainer}>
|
||||
<input onChange={handleClick} type="checkbox" role="switch" name='toggle' />
|
||||
<div className={style.label}>{label}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toggle;
|
|
@ -0,0 +1,38 @@
|
|||
'use client'
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import style from './Wrapper.module.scss';
|
||||
|
||||
import Gallery from './Gallery';
|
||||
import Toggle from './Toggle';
|
||||
import { GalleryData } from '@/meta/data';
|
||||
import NoData from './NoData';
|
||||
|
||||
interface Props {
|
||||
data: GalleryData[]
|
||||
locale: string
|
||||
labels: {
|
||||
toggle: string
|
||||
noData: string
|
||||
}
|
||||
}
|
||||
|
||||
const Wrapper: React.FC<Props> = (props) => {
|
||||
|
||||
const { labels, locale, data } = props
|
||||
|
||||
const [show, setShow] = useState<boolean>(false)
|
||||
|
||||
return (
|
||||
<div className={style.resultsContainer}>
|
||||
{(data && data.length > 0) && <>
|
||||
<Toggle label={labels.toggle} handleClick={() => setShow(!show)} />
|
||||
<Gallery data={data} locale={locale} show={show} />
|
||||
</>}
|
||||
{(!data || data.length == 0) && <NoData msg={labels.noData}/>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Wrapper;
|
|
@ -0,0 +1,22 @@
|
|||
import React, { } from 'react';
|
||||
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
|
||||
import Wrapper from './Wrapper';
|
||||
import { GalleryData } from '@/meta/data';
|
||||
|
||||
interface Props {
|
||||
data: GalleryData[]
|
||||
}
|
||||
|
||||
const Results: React.FC<Props> = (props) => {
|
||||
|
||||
const { data } = props
|
||||
|
||||
const t = useTranslations('Results');
|
||||
const locale = useLocale()
|
||||
|
||||
return (<Wrapper data={data} locale={locale} labels={{ toggle: t('toggle'), noData: t('noData') }} />);
|
||||
};
|
||||
|
||||
export default Results;
|
|
@ -0,0 +1,30 @@
|
|||
@import 'spacing';
|
||||
@import 'colors';
|
||||
|
||||
.searchForm {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.query{
|
||||
flex: 6;
|
||||
margin-right: $spacing_16;
|
||||
}
|
||||
|
||||
.submitBtn{
|
||||
flex: 1;
|
||||
background-color: $colors_yellow;
|
||||
border-color: $colors_yellow;
|
||||
color: $colors_yellow_inverse;
|
||||
}
|
||||
|
||||
.submitBtn:hover {
|
||||
background-color: $colors_yellow_hover;
|
||||
border-color: $colors_yellow_hover;
|
||||
}
|
||||
|
||||
.submitBtn:focus {
|
||||
background-color: $colors_yellow_focus;
|
||||
border-color: $colors_yellow_focus;
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
'use client'
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import style from './SearchBarForm.module.scss'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
interface Props {
|
||||
query?: string
|
||||
locale: string
|
||||
labels: {
|
||||
query: { placeholder: string }
|
||||
submit: { value: string }
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_SEARCH_LENGTH = 50;
|
||||
|
||||
const SearchBarForm: React.FC<Props> = (props) => {
|
||||
|
||||
const { query, labels, locale } = props;
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
//@ts-ignore
|
||||
const query = event.target.query.value
|
||||
|
||||
if (query.length > 0) {
|
||||
router.push(`/${locale}/search/${query}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={style.searchForm} method="get" action="/">
|
||||
<input className={style.query} type="text" size={MAX_SEARCH_LENGTH} name="query" placeholder={labels.query.placeholder} defaultValue={query ?? undefined} />
|
||||
<input className={style.submitBtn} type="submit" value={labels.submit.value} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBarForm;
|
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import SearchBarForm from './SearchBarForm';
|
||||
|
||||
interface Props {
|
||||
query?: string
|
||||
}
|
||||
|
||||
const SearchBar: React.FC<Props> = (props) => {
|
||||
|
||||
const { query } = props;
|
||||
|
||||
const t = useTranslations('Search');
|
||||
const locale = useLocale()
|
||||
|
||||
return (
|
||||
<SearchBarForm query={query} locale={locale} labels={{
|
||||
query: { placeholder: t('placeholder')},
|
||||
submit: { value: t('submit')}
|
||||
}} />
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBar;
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
|
||||
const Layout: React.FC<React.PropsWithChildren> = (props) => {
|
||||
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<main className="container">
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
|
||||
import Header from '@/components/Layout/Header';
|
||||
import SearchBar from '@/components/Layout/SearchBar';
|
||||
import Results from '@/components/Layout/Results';
|
||||
|
||||
import { GalleryData } from '@/meta/data';
|
||||
|
||||
interface Props {
|
||||
data: GalleryData[]
|
||||
}
|
||||
|
||||
const Home: React.FC<Props> = (props) => {
|
||||
|
||||
const { data } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<SearchBar />
|
||||
<Results data={data} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
|
||||
import Header from '@/components/Layout/Header';
|
||||
import SearchBar from '@/components/Layout/SearchBar';
|
||||
import Results from '@/components/Layout/Results';
|
||||
import { GalleryData } from '@/meta/data';
|
||||
|
||||
interface Props {
|
||||
query: string
|
||||
data: GalleryData[]
|
||||
}
|
||||
|
||||
const Search: React.FC<Props> = (props) => {
|
||||
|
||||
const { query, data } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<SearchBar query={query} />
|
||||
<Results data={data} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
|
||||
import Header from '@/components/Layout/Header';
|
||||
import Player from '@/components/Layout/Player';
|
||||
import SearchBar from '@/components/Layout/SearchBar';
|
||||
import Results from '@/components/Layout/Results';
|
||||
|
||||
import { GalleryData, VideoData } from '@/meta/data';
|
||||
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
data: VideoData
|
||||
related: GalleryData[]
|
||||
}
|
||||
|
||||
const Video: React.FC<Props> = (props) => {
|
||||
|
||||
const { data, related } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Player data={data} />
|
||||
<SearchBar />
|
||||
{related && <Results data={related} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Video;
|
|
@ -0,0 +1 @@
|
|||
export const XVIDEOS_BASE_URL: string = "https://www.xvideos.com"
|
|
@ -0,0 +1,14 @@
|
|||
import {notFound} from 'next/navigation';
|
||||
import {getRequestConfig} from 'next-intl/server';
|
||||
|
||||
// Can be imported from a shared config
|
||||
const locales = ['en', 'it'];
|
||||
|
||||
export default getRequestConfig(async ({locale}) => {
|
||||
// Validate that the incoming `locale` parameter is valid
|
||||
if (!locales.includes(locale as any)) notFound();
|
||||
|
||||
return {
|
||||
messages: (await import(`../locale/${locale}.json`)).default
|
||||
};
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
export interface GalleryData {
|
||||
videoUrl: string
|
||||
imgUrl: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface VideoData {
|
||||
lowResUrl: string,
|
||||
hiResUrl?: string,
|
||||
hlsUrl?: string
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import createMiddleware from 'next-intl/middleware';
|
||||
|
||||
export default createMiddleware({
|
||||
// A list of all locales that are supported
|
||||
locales: ['en', 'it'],
|
||||
|
||||
// Used when no locale matches
|
||||
defaultLocale: 'it'
|
||||
});
|
||||
|
||||
export const config = {
|
||||
// Match only internationalized pathnames
|
||||
matcher: ['/', '/(en|it)/:path*']
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
$portrait: 40em;
|
||||
$tablet: 48em;
|
||||
$laptop: 64em;
|
||||
$desktop: 80em;
|
|
@ -0,0 +1,5 @@
|
|||
// primaries
|
||||
$colors_yellow: #fdd835;
|
||||
$colors_yellow_hover: #fbc02d;
|
||||
$colors_yellow_focus: rgba(253, 216, 53, 0.125);
|
||||
$colors_yellow_inverse: rgba(0, 0, 0, 0.75);
|
|
@ -0,0 +1,4 @@
|
|||
$spacing_4: 4px;
|
||||
$spacing_8: 8px;
|
||||
$spacing_16: 16px;
|
||||
$spacing_32: 32px;
|
|
@ -0,0 +1 @@
|
|||
@use "@picocss/pico";
|
|
@ -0,0 +1,53 @@
|
|||
import { XVIDEOS_BASE_URL } from '@/constants/urls';
|
||||
import { GalleryData, VideoData } from '@/meta/data';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
|
||||
interface FetchParams {
|
||||
baseUrl?: string
|
||||
query?: string
|
||||
}
|
||||
|
||||
export const fetchGalleryData = async (params?: FetchParams): Promise<GalleryData[]> => {
|
||||
|
||||
let data: GalleryData[] = [];
|
||||
|
||||
const reqHeaders = {
|
||||
headers: {
|
||||
"User-Agent": 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5397.215 Safari/537.36'
|
||||
},
|
||||
};
|
||||
|
||||
const queryUrl = `${(params && params.baseUrl) ?? XVIDEOS_BASE_URL}${params && params.query ? '/?k=' + params.query : ''}`
|
||||
|
||||
await axios.get(queryUrl, reqHeaders)
|
||||
|
||||
.then(response => {
|
||||
|
||||
const html = response.data;
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const thumbs = $(".thumb-block");
|
||||
|
||||
thumbs.map((key, thumb) => {
|
||||
|
||||
const videoUrl = $(thumb).find(".thumb a").attr("href")
|
||||
const imgUrl = $(thumb).find(".thumb img").attr("data-src")
|
||||
const text = $(thumb).find(".thumb-under a").attr("title")
|
||||
|
||||
videoUrl && imgUrl && text && data.push({
|
||||
videoUrl,
|
||||
imgUrl,
|
||||
text
|
||||
})
|
||||
})
|
||||
|
||||
}).catch((error: AxiosError) => {
|
||||
// handle errors
|
||||
});
|
||||
|
||||
|
||||
return data
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import { XVIDEOS_BASE_URL } from '@/constants/urls';
|
||||
import { GalleryData, VideoData } from '@/meta/data';
|
||||
|
||||
import axios, { AxiosError } from 'axios';
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import { findRelatedVideos, findVideoUrlInsideTagStringByFunctionNameAndExtension } from '../string';
|
||||
|
||||
interface FetchParams {
|
||||
baseUrl?: string
|
||||
query?: string
|
||||
}
|
||||
|
||||
export const fetchVideoData = async (videoId: string, params?: FetchParams): Promise<[VideoData, GalleryData[]]> => {
|
||||
|
||||
let data: VideoData = {
|
||||
lowResUrl: ''
|
||||
}
|
||||
|
||||
let related: GalleryData[] = [];
|
||||
|
||||
const reqHeaders = {
|
||||
headers: {
|
||||
"User-Agent": 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5397.215 Safari/537.36'
|
||||
},
|
||||
};
|
||||
|
||||
const queryUrl = `${(params && params.baseUrl) ?? XVIDEOS_BASE_URL}${videoId}`
|
||||
|
||||
await axios.get(queryUrl, reqHeaders)
|
||||
|
||||
.then(response => {
|
||||
|
||||
const html = response.data;
|
||||
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const scriptTags = $("script");
|
||||
|
||||
// populate video data object
|
||||
scriptTags.map((idx, elem) => {
|
||||
|
||||
const lowResUrl = findVideoUrlInsideTagStringByFunctionNameAndExtension($(elem).toString(), 'setVideoUrlLow', '.mp4')
|
||||
const hiResUrl = findVideoUrlInsideTagStringByFunctionNameAndExtension($(elem).toString(), 'setVideoUrlHigh', '.mp4')
|
||||
const hlsUrl = findVideoUrlInsideTagStringByFunctionNameAndExtension($(elem).toString(), 'setVideoHLS', '.m3u8')
|
||||
|
||||
if (lowResUrl) {
|
||||
data.lowResUrl = lowResUrl;
|
||||
}
|
||||
|
||||
if (hiResUrl) {
|
||||
data.hiResUrl = hiResUrl
|
||||
}
|
||||
|
||||
if (hlsUrl) {
|
||||
data.hlsUrl = hlsUrl
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// populate related gallery
|
||||
scriptTags.map((idx, elem) => {
|
||||
const relatedVideos = findRelatedVideos($(elem).toString())
|
||||
|
||||
if (relatedVideos) {
|
||||
related = relatedVideos
|
||||
}
|
||||
})
|
||||
|
||||
}).catch((error: AxiosError) => {
|
||||
// handle errors
|
||||
});
|
||||
|
||||
return [data, related];
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { GalleryData } from "@/meta/data";
|
||||
|
||||
export const findVideoUrlInsideTagStringByFunctionNameAndExtension = (
|
||||
tagBlock: string, functionName: string, extension: string): string|null => {
|
||||
const start = tagBlock.indexOf(`html5player.${functionName}('`) + `html5player.${functionName}('`.length;
|
||||
const end = tagBlock.toString().indexOf("'", start);
|
||||
|
||||
const substr = tagBlock.substring(start, end);
|
||||
|
||||
if (substr.includes(extension)) {
|
||||
return substr
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const findRelatedVideos = (tagBlock: string): GalleryData[]|null => {
|
||||
if (!(tagBlock.includes('video_related=['))) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Trova l'inizio e la fine dell'array di oggetti nell'input
|
||||
const start = tagBlock.indexOf('[{');
|
||||
const end = tagBlock.lastIndexOf('}]') + 2;
|
||||
|
||||
// Estrai la sottostringa contenente l'array di oggetti
|
||||
const jsonString = tagBlock.substring(start, end);
|
||||
|
||||
// Parsea la stringa JSON in un array di oggetti
|
||||
const videoRelatedArray = JSON.parse(jsonString);
|
||||
|
||||
// Mappa ogni oggetto nell'array per rinominare le chiavi
|
||||
//@ts-ignore
|
||||
const parsedArray = videoRelatedArray.map(obj => ({
|
||||
//@ts-ignore
|
||||
videoUrl: obj.u,
|
||||
imgUrl: obj.i,
|
||||
text: obj.tf
|
||||
}));
|
||||
|
||||
return parsedArray;
|
||||
}
|
|
@ -23,4 +23,4 @@
|
|||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue