diff --git a/README.md b/README.md index c82a2dd..0200401 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,8 @@ Initial load 850-1000ms ### Metrics as of 4/14/2025 -- Browser memory allocation is appropriate and doesn't increase over time with no activity after 72 hours. -- Memory rises with video play normally, falls back to baseline in under 2 min from watch page unmounting. +- Browser memory allocation is appropriate and doesn't increase over time with no activity. +- Memory rises with video play normally, falls back to above numbers in under 2 min from watch page unmounting - ~42-72MB heap size depending on how much is cached by react query (depends on usage) - 99% performance by Lighthouse - 0.0 CLS diff --git a/dev-dist/sw.js b/dev-dist/sw.js index bdb0051..7286f32 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -85,7 +85,7 @@ define(['./workbox-f6195dc0'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "/index.html", - "revision": "0.417jg7a7vso" + "revision": "0.tj0tk14ldd" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/index.html"), { diff --git a/src/index.css b/src/index.css index b8d986f..d3700e2 100644 --- a/src/index.css +++ b/src/index.css @@ -117,7 +117,7 @@ footer { body { background-color: black; - + overflow-x: hidden; } @@ -128,9 +128,7 @@ header { } -nav { - scrollbar-gutter: stable !important -} + .swiper { width: 100%; diff --git a/src/main.tsx b/src/main.tsx index e0a840e..77c2843 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -21,10 +21,11 @@ import TextSkeleton from './components/loadingSkeletons/TextSkeleton.tsx'; import HistoryPageSkeleton from './components/loadingSkeletons/HistoryPageSkeleton.tsx'; import ScrollToTop from './components/helpers/ScrollToTop.tsx'; -const TvUpcoming = lazy(() => import('./pages/tvPages/TvUpcoming.tsx')); const MovieUpcoming = lazy( () => import('./pages/moviePages/MovieUpcoming.tsx'), ); +const TvUpcoming = lazy(() => import('./pages/tvPages/TvUpcoming.tsx')); + const History = lazy(() => import('./pages/watchPages/History.tsx')); const CastMemberDetail = lazy( () => import('./pages/detailPages/CastMemberDetail.tsx'), @@ -215,6 +216,7 @@ const router = createBrowserRouter([ ), }, + { path: 'tv/:series_id', element: ( diff --git a/src/pages/FAQ.tsx b/src/pages/FAQ.tsx index a169fc2..0d51051 100644 --- a/src/pages/FAQ.tsx +++ b/src/pages/FAQ.tsx @@ -38,12 +38,13 @@ const FAQPage = () => { const faqData = [ { question: 'Do I have to pay anything to use your site?', - answer: 'BingeBox is 100% free. ', + answer: + 'BingeBox is 100% free. We may start accepting donations in the future, strictly for your appreciation of the site, not the content.', }, { question: 'What about all these popups & redirects?', answer: - "We aren't putting those on you, they come from the servers we link to that allow you to stream content. To some extent it is understandable that content providers need money coming in, but we can't always be sure that the popups are safe or appropriate. If you want to block the popups, the recommendations are as follows: 1) If you are on Safari IOS mobile, you can go into settings for Safari, and under advanced options, then feature flags, you can find a toggle to verify the window.open function. This will stop everything and some of your taps will appear to do nothing. This is the absolute most comprehensive approach. 2) On Chrome desktop, you can add poper blocker. 3) For Android, we think Firefox + uBlock is a good option. 4) Brave browser will help but on iOS it can't stop all the pop-ups and honestly isn't nearly as good as #1. 4) We are working on ad blocking options you will be able to opt into later.", + "We aren't putting those on you, they come from the servers we link to that allow you to stream content. To some extent it is understandable that content providers need money coming in, but we can't always be sure that the popups are safe or appropriate. If you want to block the popups, the recommendations are as follows: 1) If you are on Safari IOS mobile, you can go into settings for Safari, and under advanced options, then feature flags, you can find a toggle to verify the window.open function. This will stop almost everything (100% all redirects/popups on SOME servers) and some of your taps will appear to do nothing. This is the absolute most comprehensive approach. 2) On Chrome or Firefox desktop, you can add Poper Blocker. 3) For Android, we think Firefox + uBlock is a good option. 4) Brave browser is another option in \"Shields Up\" mode but on iOS it can't stop all the pop-ups & redirects and isn't nearly as good as #1. 4) We are working on ad blocking options you will be able to opt into later.", }, { question: diff --git a/src/pages/watchPages/AdFreeTestMovie.tsx b/src/pages/watchPages/AdFreeTestMovie.tsx new file mode 100644 index 0000000..db1f2c1 --- /dev/null +++ b/src/pages/watchPages/AdFreeTestMovie.tsx @@ -0,0 +1,196 @@ +import { useParams } from 'react-router-dom'; +import { useWatchDetails } from '../../hooks/useItemOrWatchDetail'; +import WatchDescription from '../../components/WatchDescription'; +import BackButton from '../../components/buttons/BackBtn'; +import FullscreenBtn from '../../components/buttons/FullScreenBtn'; +import ServerList from '../../components/lists/ServerList'; +import { isIphoneSafari, isIPad } from '../../utils/helpers'; +import serverData from '../../utils/data/servers.json'; +import { useEffect, useState, useRef } from 'react'; +import dayjs from 'dayjs'; +import useDocumentTitle from '../../hooks/usePageTitles'; +import { useStore } from '../../state/store'; + +const MovieAdFree = () => { + const { addToContinueWatchingMovie } = useStore(); + const { movie_id } = useParams<{ movie_id: string }>(); + const { data: movie } = useWatchDetails('movie', movie_id ?? ''); + const { servers } = serverData; + + const iframeRef = useRef(null); + const timeoutRef = useRef(null); + useDocumentTitle( + movie?.title + ? `Watch ${movie?.title || 'Movie'} | BingeBox` + : 'Loading... | BingeBox', + ); + + const [isLoading, setIsLoading] = useState(false); + const [selectedServer, setSelectedServer] = useState(() => { + const lastSelectedServer = localStorage.getItem('lastSelectedServer'); + return lastSelectedServer || servers[0].value; + }); + + useEffect(() => { + if (!movie) return; + // setTimeout(() => { + addToContinueWatchingMovie( + Number(movie_id), + 'movie', + dayjs().unix(), + movie.title, + movie.backdrop_path, + movie.release_date, + movie.runtime, + ); + // }, 180000); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [movie_id, movie]); + + useEffect(() => { + let newURL = ''; + switch (selectedServer) { + case 'vidsrc.xyz': + newURL = `https://bingebox-server-54dc60d03f7d.herokuapp.com/api/video/movie/${movie_id}`; + break; + case 'videasy.net': + newURL = `https://player.videasy.net/movie/${movie_id}`; + break; + case 'vidlink.pro': + newURL = `https://vidlink.pro/movie/${movie_id}`; + break; + case 'moviesapi.club': + newURL = `https://moviesapi.club/movie/${movie_id}`; + break; + case 'embed.su': + newURL = `https://embed.su/embed/movie/${movie_id}`; + break; + case 'nontongo.win': + newURL = `https://www.nontongo.win/embed/movie/${movie_id}`; + break; + case 'vidsrc.wtf': + newURL = `https://vidsrc.wtf/api/3/movie/?id=${movie_id}`; + break; + case 'vidsrc.wtf-ml': + newURL = `https://vidsrc.wtf/api/2/movie/?id=${movie_id}`; + break; + case '111movies.com': + newURL = ` https://111movies.com/movie/${movie_id}`; + break; + case 'vidfast.pro': + newURL = `https://vidfast.pro/movie/${movie_id}`; + break; + case 'superembed.stream': + newURL = ` https://multiembed.mov/directstream.php?video_id=${movie_id}&tmdb=1`; + break; + } + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + setIsLoading(true); + if (iframeRef.current) { + iframeRef.current.contentWindow?.location.replace('about:blank'); + } + setTimeout(() => { + iframeRef.current?.contentWindow?.location.replace(newURL); + timeoutRef.current = setTimeout(() => { + setIsLoading(false); + }, 750); + }, 300); + + localStorage.setItem('lastSelectedServer', selectedServer); + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + }, [selectedServer, movie_id]); + + return ( +
+
+
+
+
+ +
+ {movie && ( +

+ {movie.title || ''} +

+ )} + {/* iphone safari doesn't support the FS api */} +
+ +
+
+
+
+ + {isLoading && ( +
+
+
+

+ Loading{' '} + { + servers.find( + (server) => server.value === selectedServer, + )?.name + } + ...{' '} +

+
+
+ )} +
+ + {/* description */} +
+ {movie && ( + + )} +
+
+
+
+ {/* right side with server choices and episodes for tv*/} +
+ +
+
+
+
+ ); +}; + +export default MovieAdFree; diff --git a/src/pages/watchPages/AdFreeTestTV.tsx b/src/pages/watchPages/AdFreeTestTV.tsx new file mode 100644 index 0000000..1b828be --- /dev/null +++ b/src/pages/watchPages/AdFreeTestTV.tsx @@ -0,0 +1,522 @@ +import { useParams } from 'react-router-dom'; +import { + useWatchDetails, + useTVSeasonEpisodes, +} from '../../hooks/useItemOrWatchDetail'; +import WatchDescription from '../../components/WatchDescription'; +import BackButton from '../../components/buttons/BackBtn'; +import FullscreenBtn from '../../components/buttons/FullScreenBtn'; +import WatchPrevBtn from '../../components/buttons/WatchPrevBtn'; +import WatchNextBtn from '../../components/buttons/WatchNextBtn'; +import ListBoxComp from '../../components/selectors/ListBox'; +import serverData from '../../utils/data/servers.json'; +import { useEffect, useState, useRef } from 'react'; +import { Settings } from 'lucide-react'; +import SeasonNavigation from '../../components/buttons/SeasonNavigation'; +import { isIPad, isIphoneSafari } from '../../utils/helpers'; +import EpisodeList from '../../components/lists/EpisodeList'; +import dayjs from 'dayjs'; +import useDocumentTitle from '../../hooks/usePageTitles'; +import { useStore } from '../../state/store'; + +const AdFreeWatchTv = () => { + const VIEWING_PROGRESS_LIMIT = 250; + const { servers } = serverData; + const { addToContinueWatchingTv } = useStore(); + const iframeRef = useRef(null); + const timeoutRef = useRef(null); + const iframeLoadRef = useRef(null); + const { series_id } = useParams<{ series_id: string }>(); + const [isLoading, setIsLoading] = useState(true); + + const [selectedServer, setSelectedServer] = useState(() => { + const lastSelectedServer = localStorage.getItem('lastSelectedServer'); + return lastSelectedServer || servers[0].value; + }); + + const [viewProgress] = useState(() => { + const viewProgressObj = localStorage.getItem(`viewing-progress`); + if (viewProgressObj) { + const items = JSON.parse(viewProgressObj); + const progressItem = items[`tv-${series_id}`]; + if (progressItem) { + return { + [`tv-${series_id}`]: { + season: Number(progressItem.season), + episode: Number(progressItem.episode), + lastUpdated: Number(progressItem.lastUpdated), + }, + }; + } + return null; + } + return null; + }); + + const [selectedSeason, setSelectedSeason] = useState(() => { + if (viewProgress) { + const selectedSeason = viewProgress[`tv-${series_id}`]?.season; + if (selectedSeason) { + return Number(selectedSeason); + } + return 1; + } + return 1; + }); + + const [selectedEpisode, setSelectedEpisode] = useState(() => { + if (viewProgress) { + const selectedEpisode = viewProgress[`tv-${series_id}`]?.episode; + if (selectedEpisode) { + return Number(selectedEpisode); + } + return 1; + } + return 1; + }); + + const [currentSeasonLength, setCurrentSeasonLength] = useState(0); + const [previousSeasonLength, setPreviousSeasonLength] = useState(0); + + const prevServerRef = useRef(selectedServer); + + const { data: series } = useWatchDetails('tv', series_id!); + const { data: episodes } = useTVSeasonEpisodes( + series_id ?? '', + String(selectedSeason), + ); + useDocumentTitle( + series?.original_name + ? `Watch ${series?.original_name || 'TV Show'} | BingeBox` + : 'Loading... | BingeBox', + ); + const [unlocked, setUnlocked] = useState(false); + const interactionTimeoutRef = useRef(null); + + const handleMouseMove = (e: MouseEvent) => { + const el = document.elementFromPoint(e.clientX, e.clientY); + if (!el) return; + + const cursor = getComputedStyle(el).cursor; + // cursor is arrow when clickjack overlay is on + if (cursor === 'pointer' && !unlocked) { + console.log('Cursor is pointer. Unlocking iframe interaction.'); + + setUnlocked(true); + + if (interactionTimeoutRef.current) + clearTimeout(interactionTimeoutRef.current); + interactionTimeoutRef.current = setTimeout(() => { + setUnlocked(false); + console.log('Locking iframe interaction again.'); + }, 1000); //unlock for 1 second + } + }; + useEffect(() => { + window.addEventListener('mousemove', handleMouseMove); + + return () => { + window.removeEventListener('mousemove', handleMouseMove); + + if (interactionTimeoutRef.current) { + console.log('Clearing timeout...'); + clearTimeout(interactionTimeoutRef.current); + interactionTimeoutRef.current = null; + } + }; + }, []); + + useEffect(() => { + if (!series) return; + + // setTimeout(() => { + addToContinueWatchingTv( + Number(series_id!), + 'tv', + dayjs().unix(), + series.original_name, + selectedSeason, + selectedEpisode, + series.backdrop_path, + ); + + // }, 180000); + // es-lint-disable-next-line react-hooks/exhaustive-deps + }, [series_id, series, selectedSeason, selectedEpisode]); + + useEffect(() => { + if (episodes) { + // Shift previous season length when moving to a new season + setPreviousSeasonLength(currentSeasonLength); + setCurrentSeasonLength(episodes?.episodes?.length); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedSeason, episodes]); + + useEffect(() => { + const updatedViewProgressItem = { + [`tv-${series_id}`]: { + season: selectedSeason, + episode: selectedEpisode, + lastUpdated: dayjs().unix(), + }, + }; + const viewProgressObj = localStorage.getItem(`viewing-progress`); + if (viewProgressObj) { + const viewProgress = JSON.parse(viewProgressObj); + // keep a rotation of 250 tv series in local storage and remove the oldest in favor of the most recent + if (Object.keys(viewProgress).length > VIEWING_PROGRESS_LIMIT) { + const oldestKey = Object.keys(viewProgress).reduce((oldest, key) => { + if (!viewProgress[oldest].lastUpdated) return key; + if (!viewProgress[key].lastUpdated) return oldest; + return viewProgress[key].lastUpdated < + viewProgress[oldest].lastUpdated + ? key + : oldest; + }, Object.keys(viewProgress)[0]); + delete viewProgress[oldestKey]; + } + const updatedViewProgress = { + ...viewProgress, + ...updatedViewProgressItem, + }; + + localStorage.setItem( + `viewing-progress`, + JSON.stringify(updatedViewProgress), + ); + } else { + localStorage.setItem( + `viewing-progress`, + JSON.stringify(updatedViewProgressItem), + ); + } + }, [series_id, selectedSeason, selectedEpisode]); + + // when page is remounted, user will see loading spinner for 750ms + useEffect(() => { + timeoutRef.current = setTimeout(() => { + setIsLoading(false); + }, 750); + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + }, []); + + useEffect(() => { + let newURL = ''; + switch (selectedServer) { + case 'vidsrc.xyz': + newURL = `http://vidsrc.net/embed/tv/${series_id}/${selectedSeason}/${selectedEpisode}`; + break; + case 'videasy.net': + newURL = `https://player.videasy.net/tv/${series_id}/${selectedSeason}/${selectedEpisode}`; + break; + case 'vidlink.pro': + newURL = `https://vidlink.pro/tv/${series_id}/${selectedSeason}/${selectedEpisode}`; + break; + case 'moviesapi.club': + newURL = `https://moviesapi.club/tv/${series_id}-${selectedSeason}-${selectedEpisode}`; + break; + case 'embed.su': + newURL = `https://embed.su/embed/tv/${series_id}/${selectedSeason}/${selectedEpisode}`; + break; + case 'nontongo.win': + newURL = `https://www.nontongo.win/embed/tv/${series_id}/${selectedSeason}/${selectedEpisode}`; + break; + case 'vidsrc.wtf': + newURL = `https://vidsrc.wtf/api/3/tv/?id=${series_id}&s=${selectedSeason}&e=${selectedEpisode}`; + break; + case 'vidsrc.wtf-ml': + newURL = `https://vidsrc.wtf/api/2/tv/?id=${series_id}&s=${selectedSeason}&e=${selectedEpisode}`; + break; + case '111movies.com': + newURL = `https://111movies.com/tv/${series_id}/${selectedSeason}/${selectedEpisode}`; + break; + case 'vidfast.pro': + newURL = `https://vidfast.pro/tv/${series_id}/${selectedSeason}/${selectedEpisode}`; + break; + case 'superembed.stream': + newURL = `https://multiembed.mov/directstream.php?video_id=${series_id}&tmdb=1&s=${selectedSeason}&e=${selectedEpisode}`; + break; + case 'vidsrc.xyz.adfree': + newURL = `/api/video/tv/${series_id}/${selectedSeason}/${selectedEpisode}`; + break; + } + + const serverChanged = prevServerRef.current !== selectedServer; + prevServerRef.current = selectedServer; + + if (serverChanged) { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + setIsLoading(true); + + if (iframeRef.current) { + iframeRef.current.contentWindow?.location.replace('about:blank'); + } + + iframeLoadRef.current = setTimeout(() => { + iframeRef.current?.contentWindow?.location.replace(newURL); + timeoutRef.current = setTimeout(() => { + setIsLoading(false); + }, 750); + }, 300); + + localStorage.setItem('lastSelectedServer', selectedServer); + } else { + iframeRef.current?.contentWindow?.location.replace(newURL); + } + + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + if (iframeLoadRef.current) { + clearTimeout(iframeLoadRef.current); + } + }; + }, [selectedServer, series_id, selectedSeason, selectedEpisode]); + + return ( +
+
+
+
+
+ +
+ {series && ( +

+ {series.original_name || ''} +

+ )} + +
+ +
+
+
+
+ {selectedServer === 'vidsrc.xyz.adfree' ? ( + + ) : ( + <> + {!unlocked && ( + // overlay that absorbs 'bad' clicks based on cursor state +
+ )} + + + )} + {isLoading && ( +
+
+
+

+ Loading{' '} + { + servers.find( + (server) => server.value === selectedServer, + )?.name + } + ...{' '} +

+
+
+ )} +
+ {/* player controls (for tv) */} + {series && ( +
+
+
+
+
+ + Season {selectedSeason} • Episode{' '} + {selectedEpisode} + + {episodes ? ( + + {episodes?.episodes?.[selectedEpisode - 1]?.name} + + ) : ( + + Loading... + + )} +
+
+ {episodes ? ( +
+ + +
+ ) : ( +
+ + +
+ )} +
+
+
+
+ )} + +
+ {/* description */} + {series && ( + + )} +
+
+
+ {/* Sidebar */} +
+
+
+
+ {/* server selection */} + + +
+

Change Server

+

+ { + servers.find( + (server) => server.value === selectedServer, + )?.name + } +

+
+
+ } + selectedOption={selectedServer} + setSelectedOption={setSelectedServer} + availableOptions={servers} + /> +
+
+ {/* season nav here */} + +
+
+
+ {/* episode list here */} + {episodes && ( + + )} +
+
+
+
+ + ); +}; + +export default AdFreeWatchTv; diff --git a/src/pages/watchPages/WatchMovie.tsx b/src/pages/watchPages/WatchMovie.tsx index f5b456f..5294b56 100644 --- a/src/pages/watchPages/WatchMovie.tsx +++ b/src/pages/watchPages/WatchMovie.tsx @@ -138,6 +138,12 @@ const WatchMovie = () => { case 'superembed.stream': newURL = ` https://multiembed.mov/directstream.php?video_id=${movie_id}&tmdb=1`; break; + case 'vidsrc.xyz.safe': + newURL = `https://bingebox-server-54dc60d03f7d.herokuapp.com/api/video/movie/${movie_id}`; + break; + case 'videasy.net.safe': + newURL = `https://player.videasy.net/movie/${movie_id}`; + break; } if (timeoutRef.current) { @@ -191,21 +197,38 @@ const WatchMovie = () => {
- {/* {!unlocked && ( - // overlay that absorbs 'bad' clicks based on cursor state -
- )} */} - + {selectedServer === 'vidsrc.xyz.safe' || + selectedServer === 'videasy.net.safe' ? ( + + ) : ( + <> + {/* {!unlocked && ( + // overlay that absorbs 'bad' clicks based on cursor state +
+ )} */} + + + )} {isLoading && (
diff --git a/src/pages/watchPages/WatchTV.tsx b/src/pages/watchPages/WatchTV.tsx index ac9fb4e..e1516c6 100644 --- a/src/pages/watchPages/WatchTV.tsx +++ b/src/pages/watchPages/WatchTV.tsx @@ -243,6 +243,12 @@ const WatchTV = () => { case 'superembed.stream': newURL = `https://multiembed.mov/directstream.php?video_id=${series_id}&tmdb=1&s=${selectedSeason}&e=${selectedEpisode}`; break; + case 'vidsrc.xyz.safe': + newURL = `https://bingebox-server-54dc60d03f7d.herokuapp.com/api/video/tv/${series_id}/${selectedSeason}/${selectedEpisode}`; + break; + case 'videasy.net.safe': + newURL = `https://player.videasy.net/tv/${series_id}/${selectedSeason}/${selectedEpisode}`; + break; } const serverChanged = prevServerRef.current !== selectedServer; @@ -314,23 +320,38 @@ const WatchTV = () => { id='video-player' className='relative pt-[56.25%] w-full overflow-hidden mb-[24px] rounded-lg bg-[#1f1f1f] min-h-[300px]' > - {/* {!unlocked && ( - // overlay that absorbs 'bad' clicks based on cursor state -
- )} */} - + {selectedServer === 'vidsrc.xyz.safe' || + selectedServer === 'videasy.net.safe' ? ( + + ) : ( + <> + {/* {!unlocked && ( + // overlay that absorbs 'bad' clicks based on cursor state +
+ )} */} + + + )} {isLoading && (
diff --git a/src/utils/data/providers.json b/src/utils/data/providers.json index 1e74d7b..b94425e 100644 --- a/src/utils/data/providers.json +++ b/src/utils/data/providers.json @@ -125,14 +125,8 @@ "name": "BBC America", "value": "397" }, - { "id": 25, - "name": "ESPN", - "value": "1718" - }, - { - "id": 26, "name": "BET+", "value": "1759" } diff --git a/src/utils/data/servers.json b/src/utils/data/servers.json index bed60c9..0f3acb2 100644 --- a/src/utils/data/servers.json +++ b/src/utils/data/servers.json @@ -51,6 +51,16 @@ "id": 10, "name": "Server 10", "value": "superembed.stream" + }, + { + "id": 11, + "name": "Safe Server 1", + "value": "vidsrc.xyz.safe" + }, + { + "id": 12, + "name": "Safe Server 2", + "value": "videasy.net.safe" } diff --git a/vite.config.ts b/vite.config.ts index e179753..9b4d839 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -118,8 +118,9 @@ export default defineConfig({ host: true, proxy: { '/api': { - target: 'https://api.offlinetv.net', + target: 'http://localhost:3001', changeOrigin: true, + secure: false, }, }, },